diff --git a/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch b/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch new file mode 100644 index 00000000000..972ba99e930 --- /dev/null +++ b/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch @@ -0,0 +1,21 @@ +diff --git a/lib/JSTransformer.js b/lib/JSTransformer.js +index c2a45a9665b4b88836d720669b997872082bb143..f46b0edf814e398ed2a2584efeb040c4b77d58db 100644 +--- a/lib/JSTransformer.js ++++ b/lib/JSTransformer.js +@@ -447,14 +447,8 @@ var _default = exports.default = new (_plugin().Transformer)({ + }, + loc: { + filePath: asset.filePath, +- start: { +- line: loc.start_line + Number(asset.meta.startLine ?? 1) - 1, +- column: loc.start_col +- }, +- end: { +- line: loc.end_line + Number(asset.meta.startLine ?? 1) - 1, +- column: loc.end_col +- } ++ line: loc.line, ++ col: loc.col + }, + invalidateOnFileChange(filePath) { + asset.invalidateOnFileChange(filePath); diff --git a/eslint.config.mjs b/eslint.config.mjs index c1ac2344dd9..2120c6213e0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -500,4 +500,12 @@ export default [{ rules: { "react/react-in-jsx-scope": OFF, }, -}]; +}, { + files: ["packages/dev/style-macro-chrome-plugin/**"], + languageOptions: { + globals: { + ...globals.webextensions, + ...globals.browser + } + } +}]; \ No newline at end of file diff --git a/package.json b/package.json index cd51b2ecd1c..d9919498fb7 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,8 @@ "remark-parse": "patch:remark-parse@npm%3A10.0.1#~/.yarn/patches/remark-parse-npm-10.0.1-e654d7df78.patch", "lightningcss": "1.30.1", "react-server-dom-parcel": "canary", - "react-test-renderer": "19.1.0" + "react-test-renderer": "19.1.0", + "@parcel/transformer-js@npm:2.16.0": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch" }, "@parcel/transformer-css": { "cssModules": { diff --git a/packages/@react-spectrum/s2/style/__tests__/mergeStyles.test.js b/packages/@react-spectrum/s2/style/__tests__/mergeStyles.test.js index 00376090225..ae8349a616f 100644 --- a/packages/@react-spectrum/s2/style/__tests__/mergeStyles.test.js +++ b/packages/@react-spectrum/s2/style/__tests__/mergeStyles.test.js @@ -13,13 +13,17 @@ import {mergeStyles} from '../runtime'; import {style} from '../spectrum-theme'; +function stripMacro(css) { + return css.replaceAll(/ -macro-static-[0-9a-zA-Z]+/gi, '').replaceAll(/ -macro-dynamic-[0-9a-zA-Z]+/gi, ''); +} + describe('mergeStyles', () => { it('should merge styles', () => { let a = style({backgroundColor: 'red-1000', color: 'pink-100'}); let b = style({fontSize: 'body-xs', backgroundColor: 'gray-50'}); let expected = style({backgroundColor: 'gray-50', color: 'pink-100', fontSize: 'body-xs'}); let merged = mergeStyles(a, b); - expect(merged).toBe(expected); + expect(stripMacro(merged)).toBe(stripMacro(expected.toString())); }); it('should merge with arbitrary values', () => { @@ -27,6 +31,6 @@ describe('mergeStyles', () => { let b = style({fontSize: '[15px]', backgroundColor: 'gray-50'}); let expected = style({backgroundColor: 'gray-50', color: '[hotpink]', fontSize: '[15px]'}); let merged = mergeStyles(a, b); - expect(merged).toBe(expected); + expect(stripMacro(merged)).toBe(stripMacro(expected.toString())); }); }); diff --git a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js index 765df9d23da..64c118617ae 100644 --- a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js +++ b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js @@ -53,9 +53,13 @@ describe('style-macro', () => { } } +.-macro-static-EVNQL { + --macro-data: {"style":{"marginTop":{":first-child":{"default":4,"lg":8}}},"loc":"undefined:undefined:undefined"}; + } + " `); - expect(js).toMatchInlineSnapshot('" Jbs12 Jbpv12"'); + expect(js).toMatchInlineSnapshot('" Jbs12 Jbpv12 -macro-static-EVNQL"'); }); it('should support self references', () => { @@ -114,10 +118,14 @@ describe('style-macro', () => { } } +.-macro-static-qHi23 { + --macro-data: {"style":{"borderWidth":2,"paddingX":"edge-to-text","width":"calc(200px - self(borderStartWidth) - self(paddingStart))"},"loc":"undefined:undefined:undefined"}; + } + " `); - expect(js).toMatchInlineSnapshot('" _kc12 hc12 mCPFGYc12 lc12 SMBFGYc12 Rv12 ZjUQgKd12 -m_-mc12 -S_-Sv12"'); + expect(js).toMatchInlineSnapshot('" _kc12 hc12 mCPFGYc12 lc12 SMBFGYc12 Rv12 ZjUQgKd12 -m_-mc12 -S_-Sv12 -macro-static-qHi23"'); }); it('should support allowed overrides', () => { @@ -134,9 +142,9 @@ describe('style-macro', () => { color: 'green-400' }); - expect(js()).toMatchInlineSnapshot('" gw12 pg12"'); - expect(overrides).toMatchInlineSnapshot('" g8tmWqb12 pHJ3AUd12"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb12 pg12"'); + expect(js()).toMatchInlineSnapshot('" gw12 pg12 -macro-dynamic-1xxglvk"'); + expect(overrides).toMatchInlineSnapshot('" g8tmWqb12 pHJ3AUd12 -macro-static-Su6dhb"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb12 pg12 -macro-dynamic-jk90zw"'); }); it('should support allowed overrides for properties that expand into multiple', () => { @@ -151,9 +159,9 @@ describe('style-macro', () => { translateX: 40 }); - expect(js()).toMatchInlineSnapshot('" -_7PloMd-B12 __Ya12"'); - expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D12 __Ya12"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D12 __Ya12"'); + expect(js()).toMatchInlineSnapshot('" -_7PloMd-B12 __Ya12 -macro-dynamic-1nf427l"'); + expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D12 __Ya12 -macro-static-ZCkud"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D12 __Ya12 -macro-dynamic-1pnuhyr"'); }); it('should support allowed overrides for shorthands', () => { @@ -168,9 +176,9 @@ describe('style-macro', () => { padding: 40 }); - expect(js()).toMatchInlineSnapshot('" Tk12 Qk12 Sk12 Rk12"'); - expect(overrides).toMatchInlineSnapshot('" Tm12 Qm12 Sm12 Rm12"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" Tm12 Qm12 Sm12 Rm12"'); + expect(js()).toMatchInlineSnapshot('" Tk12 Qk12 Sk12 Rk12 -macro-dynamic-1w5dwn"'); + expect(overrides).toMatchInlineSnapshot('" Tm12 Qm12 Sm12 Rm12 -macro-static-FQziuc"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" Tm12 Qm12 Sm12 Rm12 -macro-dynamic-p1i90v"'); }); it("should support allowed overrides for values that aren't defined", () => { @@ -185,9 +193,9 @@ describe('style-macro', () => { minWidth: 32 }); - expect(js()).toMatchInlineSnapshot('" gE12"'); - expect(overrides).toMatchInlineSnapshot('" Nk12"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" Nk12 gE12"'); + expect(js()).toMatchInlineSnapshot('" gE12 -macro-dynamic-nl2mms"'); + expect(overrides).toMatchInlineSnapshot('" Nk12 -macro-static-pDx0l"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" Nk12 gE12 -macro-dynamic-11y5vdc"'); }); it('should support runtime conditions', () => { @@ -241,9 +249,9 @@ describe('style-macro', () => { " `); - expect(js({})).toMatchInlineSnapshot('" gH12 pt12"'); - expect(js({isHovered: true})).toMatchInlineSnapshot('" gF12 po12"'); - expect(js({isPressed: true})).toMatchInlineSnapshot('" gE12 pm12"'); + expect(js({})).toMatchInlineSnapshot('" gH12 pt12 -macro-dynamic-179ovcu"'); + expect(js({isHovered: true})).toMatchInlineSnapshot('" gF12 po12 -macro-dynamic-1i83kjb"'); + expect(js({isPressed: true})).toMatchInlineSnapshot('" gE12 pm12 -macro-dynamic-1npaxjo"'); }); it('should support nested runtime conditions', () => { @@ -284,10 +292,10 @@ describe('style-macro', () => { " `); - expect(js({})).toMatchInlineSnapshot('" gH12"'); - expect(js({isHovered: true})).toMatchInlineSnapshot('" gF12"'); - expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h12"'); - expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g312"'); + expect(js({})).toMatchInlineSnapshot('" gH12 -macro-dynamic-nl2p5j"'); + expect(js({isHovered: true})).toMatchInlineSnapshot('" gF12 -macro-dynamic-nl2nh1"'); + expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h12 -macro-dynamic-1w0viba"'); + expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g312 -macro-dynamic-nl27ia"'); }); it('should support variant runtime conditions', () => { @@ -301,9 +309,9 @@ describe('style-macro', () => { } }); - expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY12"'); - expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe12"'); - expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw12"'); + expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY12 -macro-dynamic-nl33fs"'); + expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe12 -macro-dynamic-enz676"'); + expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw12 -macro-dynamic-nl3sna"'); }); it('supports runtime conditions nested inside css conditions', () => { @@ -337,8 +345,8 @@ describe('style-macro', () => { " `); - expect(js({})).toMatchInlineSnapshot('" plb12"'); - expect(js({isSelected: true})).toMatchInlineSnapshot('" ple12"'); + expect(js({})).toMatchInlineSnapshot('" plb12 -macro-dynamic-1w7i5ba"'); + expect(js({isSelected: true})).toMatchInlineSnapshot('" ple12 -macro-dynamic-1w7i7u1"'); }); it('should expand shorthand properties to longhands', () => { @@ -346,7 +354,7 @@ describe('style-macro', () => { padding: 24 }); - expect(js).toMatchInlineSnapshot('" Th12 Qh12 Sh12 Rh12"'); + expect(js).toMatchInlineSnapshot('" Th12 Qh12 Sh12 Rh12 -macro-static-V268ld"'); expect(css).toMatchInlineSnapshot(` "@layer _.a; @@ -371,6 +379,10 @@ describe('style-macro', () => { } } +.-macro-static-V268ld { + --macro-data: {"style":{"padding":24},"loc":"undefined:undefined:undefined"}; + } + " `); }); @@ -389,6 +401,10 @@ describe('style-macro', () => { } } +.-macro-static-MvZuec { + --macro-data: {"style":{"backgroundColor":"blue-1000/50"},"loc":"undefined:undefined:undefined"}; + } + " `); }); @@ -410,6 +426,10 @@ describe('style-macro', () => { } } +.-macro-static-2pvxid { + --macro-data: {"style":{"--foo":{"type":"backgroundColor","value":"gray-300"}},"loc":"undefined:undefined:undefined"}; + } + " `); }); diff --git a/packages/@react-spectrum/s2/style/runtime.ts b/packages/@react-spectrum/s2/style/runtime.ts index 5b567e2ab56..9bbbfc982ff 100644 --- a/packages/@react-spectrum/s2/style/runtime.ts +++ b/packages/@react-spectrum/s2/style/runtime.ts @@ -39,16 +39,23 @@ import {StyleString} from './types'; export function mergeStyles(...styles: (StyleString | null | undefined)[]): StyleString { let definedStyles = styles.filter(Boolean) as StyleString[]; if (definedStyles.length === 1) { - return definedStyles[0]; + let first = definedStyles[0]; + if (typeof first !== 'string') { + // static macro has a toString method so that we generate the style macro map for the entry + // it's automatically called in other places, but for our merging, we have to call it ourselves + return (first as StyleString).toString() as StyleString; + } + return first; } - let map = new Map(); + let map = new Map(); for (let style of definedStyles) { - for (let [k, v] of parse(style)) { + // must call toString here for the static macro + for (let [k, v] of parse(style.toString())) { map.set(k, v); } } - + let res = ''; for (let value of map.values()) { res += value; diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index d41d6aaeee6..2e495a68468 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -176,7 +176,7 @@ export function parseArbitraryValue(value: Value): string | undefined { return value.slice(1, -1); } else if ( typeof value === 'string' && ( - /^(var|calc|min|max|clamp|round|mod|rem|sin|cos|tan|asin|acos|atan|atan2|pow|sqrt|hypot|log|exp|abs|sign)\(.+\)$/.test(value) || + /^(var|calc|min|max|clamp|round|mod|rem|sin|cos|tan|asin|acos|atan|atan2|pow|sqrt|hypot|log|exp|abs|sign)\(.+\)$/.test(value) || /^(inherit|initial|unset)$/.test(value) ) ) { @@ -205,6 +205,8 @@ interface MacroContext { addAsset(asset: {type: string, content: string}): void } +let isCompilingDependencies: boolean | null | string = false; + export function createTheme(theme: T): StyleFunction, 'default' | Extract> { let properties = new Map>(Object.entries(theme.properties).map(([k, v]) => { if (!Array.isArray(v) && v.cssProperties) { @@ -280,8 +282,11 @@ export function createTheme(theme: T): StyleFunction(theme: T): StyleFunction(); - let js = 'let rules = " ";\n'; + let js = 'let rules = " ", currentRules = {};\n'; if (allowedOverrides?.length) { for (let property of allowedOverrides) { let shorthand = theme.shorthands[property]; @@ -315,7 +320,7 @@ export function createTheme(theme: T): StyleFunction(theme: T): StyleFunction classNamePrefix(p, p)).join('|')})[^\\s]+/g`; + let regex = `/(?:^|\\s)(${[...allowedOverridesSet].map(p => classNamePrefix(p, p)).join('|')}|-macro\\$)[^\\s]+/g`; if (loop) { - js += `let matches = (overrides || '').matchAll(${regex});\n`; + js += `let matches = String(overrides || '').matchAll(${regex});\n`; js += 'for (let p of matches) {\n'; js += loop; js += ' rules += p[0];\n'; js += '}\n'; } else { - js += `rules += ((overrides || '').match(${regex}) || []).join('')\n`; + js += `rules += (String(overrides || '').match(${regex}) || []).join('')\n`; } } @@ -375,6 +380,15 @@ export function createTheme(theme: T): StyleFunction(theme: T): StyleFunction(theme: T): StyleFunction(theme: T): StyleFunction(theme: T): StyleFunction, indent?: string): string } +let conditionStack: string[] = []; + /** A CSS style rule. */ class StyleRule implements Rule { className: string; pseudos: string; property: string; value: string; + themeProperty: string | undefined; + themeValue: Value | undefined; - constructor(className: string, property: string, value: string) { + constructor(className: string, property: string, value: string, themeProperty: string, themeValue) { this.className = className; this.pseudos = ''; this.property = property; this.value = value; + if (isCompilingDependencies !== null) { + this.themeProperty = themeProperty; + this.themeValue = themeValue; + } } addPseudo(prelude: string) { @@ -687,6 +715,21 @@ class StyleRule implements Rule { res += `${indent}if (!${this.property.replace('--', '__')}) `; } res += `${indent}rules += ' ${this.className}';`; + if (this.themeProperty) { + let name = this.themeProperty; + if (this.pseudos) { + conditionStack.push(this.pseudos); + } + if (conditionStack.length) { + // name += ` (${conditionStack.join(', ')})`; + res += ` currentRules[${JSON.stringify(name)}] = typeof currentRules[${JSON.stringify(name)}] === 'object' ? currentRules[${JSON.stringify(name)}] : {"default": currentRules[${JSON.stringify(name)}]}; currentRules[${JSON.stringify(name)}][${JSON.stringify(conditionStack.join(' && '))}] = ${JSON.stringify(this.themeValue)};`; + } else { + res += ` currentRules[${JSON.stringify(name)}] = ${JSON.stringify(this.themeValue)};`; + } + if (this.pseudos) { + conditionStack.pop(); + } + } return res; } } @@ -739,10 +782,12 @@ class GroupRule implements Rule { /** A rule that applies conditionally in CSS (e.g. @media). */ class AtRule extends GroupRule { prelude: string; + themeCondition: string | null; - constructor(rules: Rule[], prelude: string, layer: string) { + constructor(rules: Rule[], prelude: string, layer: string, themeCondition: string | null) { super(rules, layer); this.prelude = prelude; + this.themeCondition = themeCondition; } toCSS(rulesByLayer: Map, preludes: string[] = [], layer?: string): void { @@ -750,6 +795,13 @@ class AtRule extends GroupRule { super.toCSS(rulesByLayer, preludes, layer); preludes?.pop(); } + + toJS(allowedOverridesSet: Set, indent?: string): string { + conditionStack.push(this.themeCondition || this.prelude); + let res = super.toJS(allowedOverridesSet, indent); + conditionStack.pop(); + return res; + } } /** A rule that applies conditionally at runtime. */ @@ -766,7 +818,10 @@ class ConditionalRule extends GroupRule { } toJS(allowedOverridesSet: Set, indent = ''): string { - return `${indent}if (props.${this.condition}) {\n${super.toJS(allowedOverridesSet, indent + ' ')}\n${indent}}`; + conditionStack.push(this.condition); + let res = `${indent}if (props.${this.condition}) {\n${super.toJS(allowedOverridesSet, indent + ' ')}\n${indent}}`; + conditionStack.pop(); + return res; } } diff --git a/packages/dev/parcel-config-storybook/package.json b/packages/dev/parcel-config-storybook/package.json index f4e45a43096..598cb5abdb6 100644 --- a/packages/dev/parcel-config-storybook/package.json +++ b/packages/dev/parcel-config-storybook/package.json @@ -13,7 +13,7 @@ "@parcel/config-default": "^2.16.0", "@parcel/core": "^2.16.0", "@parcel/resolver-storybook": ">=0.0.0", - "@parcel/transformer-js": "^2.16.0", + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch", "@parcel/transformer-react-refresh-wrap": "^2.16.0", "@parcel/transformer-storybook": ">=0.0.2" }, diff --git a/packages/dev/s2-icon-builder/package.json b/packages/dev/s2-icon-builder/package.json index 83e4792c50e..a6abec4dba9 100644 --- a/packages/dev/s2-icon-builder/package.json +++ b/packages/dev/s2-icon-builder/package.json @@ -19,7 +19,7 @@ "@parcel/plugin": "^2.16.0", "@parcel/reporter-cli": "^2.16.0", "@parcel/resolver-default": "^2.16.0", - "@parcel/transformer-js": "^2.16.0", + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch", "@parcel/transformer-raw": "^2.16.0", "@parcel/transformer-react-refresh-wrap": "^2.16.0", "@react-spectrum/parcel-namer-s2": "^0.3.2", diff --git a/packages/dev/style-macro-chrome-plugin/.parcelrc b/packages/dev/style-macro-chrome-plugin/.parcelrc new file mode 100644 index 00000000000..f497a196e5f --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/.parcelrc @@ -0,0 +1,9 @@ +{ + "extends": "@parcel/config-webextension", + "transformers": { + "*.{js,mjs,jsx,cjs,ts,tsx}": [ + "@parcel/transformer-js", + "@parcel/transformer-react-refresh-wrap" + ] + } +} diff --git a/packages/dev/style-macro-chrome-plugin/README.md b/packages/dev/style-macro-chrome-plugin/README.md new file mode 100644 index 00000000000..5add7ba038c --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/README.md @@ -0,0 +1,373 @@ +# style-macro-chrome-plugin + +This is a chrome plugin to assist in debugging the styles applied by our Style Macro. + +## Local development + +From the root of our monopackage, run + +``` +yarn +yarn workspace style-macro-chrome-plugin start +// or build to avoid refresh bugs in HMR +yarn workspace style-macro-chrome-plugin build +``` + +This will create a dist directory in the directory `packages/dev/style-macro-chrome-plugin` which will update anytime the code changes and results in a rebuild. + +Next, open Chrome and go to [chrome://extensions/](chrome://extensions/). + +Load an unpacked extension, it's a button in the top left, and navigate to the dist directory. + +The extension is now registered in Chrome and you can go to storybook or docs, wherever you are working. + +Inspect an element on the page to open dev tools and go to the Style Macro panel. + +## Troubleshooting + +If the panel isn't updating with styles, try closing the dev tools and reopening it. + +If the extension doesn't appear to have the latest code, try closing the dev tools and reopening it. You may also want to go to the extensions page and either "refresh" or remove and re-add the extension. + +If every tab you have open (or many of them) reload when you make local changes to the extension, then go into the extension settings and limit it to `localhost` or something appropriate. + +## ToDos + +- [ ] Work with RSC +- [ ] Would be pretty cool if we could match a style condition to trigger it, like hover +- [ ] Eventually add to https://github.com/astahmer/atomic-css-devtools ?? +- [ ] Our own UI ?? +- [ ] Filtering +- [ ] Resolve css variables inline +- [ ] Link to file on the side instead of grouping by filename? +- [ ] Add classname that is applying style? +- [ ] Work in MFE's + +## Extension Architecture + +This extension uses Chrome's standard extension architecture with three main components that communicate via message passing. + +### Components + +#### 1. **Page Context** (style-macro runtime + MutationObserver) +- **Location**: Runs in the actual page's JavaScript context +- **Responsibility**: + - Generates macro metadata (hash, location, styles) when style macro is evaluated + - Hosts MutationObserver that watches selected element for className changes +- **Storage**: None - static macros embed data in CSS, dynamic macros send messages +- **Communication**: + - For static macros: Embeds data in CSS custom property `--macro-data` + - For dynamic macros: Sends `window.postMessage({ action: 'update-macros', hash, loc, style })` to content script + - For className changes: Sends `window.postMessage({ action: 'class-changed', elementId })` to content script + +#### 2. **Content Script** (`content-script.js`) +- **Location**: Isolated sandboxed environment injected into the page +- **Scope**: Handles dynamic macros only (static macros are read directly from CSS) +- **Responsibility**: + - Listens for `window.postMessage({ action: 'update-macros' })` from the page and stores dynamic macro data in its own `window.__macros` + - Forwards `window.postMessage({ action: 'class-changed' })` from page to background script + - Responds to queries from DevTools (via background script), with retry logic to handle race conditions + - Cleans up stale macros every 5 minutes +- **Storage**: `window.__macros[hash] = { loc: string, style: object }` (in content script context, not page context) +- **Race Condition Handling**: When queried for a hash that doesn't exist yet, polls every 50ms for up to 500ms before responding with null +- **Communication**: + - Receives: + - `window.postMessage({ action: 'update-macros' })` from page + - `window.postMessage({ action: 'class-changed' })` from page + - `chrome.runtime.onMessage({ action: 'get-macro' })` from background + - Sends: + - `chrome.runtime.sendMessage({ action: 'update-macros' })` to background + - `chrome.runtime.sendMessage({ action: 'class-changed' })` to background + - `chrome.runtime.sendMessage({ action: 'macro-response' })` to background + +#### 3. **Background Script** (`background.js`) +- **Location**: Service worker (isolated context) +- **Responsibility**: Acts as a message broker between DevTools and content scripts +- **State**: Maintains a map of DevTools connections per tab +- **Communication**: + - Receives: + - `chrome.runtime.onConnect({ name: 'devtools-page' })` from DevTools + - `port.onMessage({ type: 'init' })` from DevTools + - `port.onMessage({ type: 'query-macros' })` from DevTools + - `chrome.runtime.onMessage({ action: 'update-macros' })` from content script + - `chrome.runtime.onMessage({ action: 'macro-response' })` from content script + - `chrome.runtime.onMessage({ action: 'class-changed' })` from content script + - Sends: + - `chrome.tabs.sendMessage({ action: 'get-macro' })` to content script + - `port.postMessage({ action: 'update-macros' })` to DevTools + - `port.postMessage({ action: 'macro-response' })` to DevTools + - `port.postMessage({ action: 'class-changed' })` to DevTools + +#### 4. **DevTools Panel** (`devtool.js`) +- **Location**: DevTools sidebar panel context +- **Responsibility**: + - Extracts macro class names from selected element: + - Static macros: `-macro-static-{hash}` → retrieves data via CSS custom properties (`--macro-data`) + - Dynamic macros: `-macro-dynamic-{hash}` → queries data via content script (responds to changing conditions) + - Queries for macro data using appropriate method based on macro type + - Displays style information in sidebar + - **Automatic Updates**: Sets up a MutationObserver on the selected element to detect className changes and automatically refreshes the panel +- **Mutation Observer**: + - Created when an element is selected via `chrome.devtools.panels.elements.onSelectionChanged` + - Watches the selected element's `class` attribute for changes + - Disconnects when: + - A new element is selected + - The DevTools connection is closed + - Triggers automatic panel refresh when className changes +- **Communication**: + - Receives: + - `port.onMessage({ action: 'macro-response' })` from background + - `port.onMessage({ action: 'update-macros' })` from background + - `port.onMessage({ action: 'class-changed' })` from background (triggers refresh) + - Sends: + - `chrome.runtime.connect({ name: 'devtools-page' })` to establish connection + - `port.postMessage({ type: 'init' })` to background + - `port.postMessage({ type: 'query-macros' })` to background (for dynamic macros only) + +### Message Flow Diagrams + +#### Flow 1a: Static Macro Lookup (DevTools reads CSS) + +Static macros are generated when style macro conditions don't change at runtime. The macro data is embedded directly into the CSS as a custom property. + +``` +┌─────────────────┐ +│ DevTools Panel │ User selects element with -macro-static-{hash} class +└────────┬────────┘ + │ Read CSS custom property --macro-data via getComputedStyle() + ↓ +┌─────────────────┐ +│ Page DOM/CSS │ Returns macro data from CSS +└────────┬────────┘ + │ { loc: "...", style: {...} } + ↓ +┌─────────────────┐ +│ DevTools Panel │ Parses and displays in sidebar +└─────────────────┘ +``` + +#### Flow 1b: Dynamic Macro Updates (Page → DevTools) + +Dynamic macros are generated when style macro conditions can change at runtime. Updates are sent via message passing. +This could be simplified and we could rely on the MutationObserver to trigger the refresh, but this way ensures +that the storage is update before we try to access the data. + +``` +┌─────────────────┐ +│ Page Context │ +│ (style-macro) │ +└────────┬────────┘ + │ window.postMessage({ action: 'update-macros', hash, loc, style }) + ↓ +┌─────────────────┐ +│ Content Script │ Stores in window.__macros[hash] +└────────┬────────┘ + │ chrome.runtime.sendMessage({ action: 'update-macros', ... }) + ↓ +┌─────────────────┐ +│ Background │ Looks up DevTools connection for tabId +└────────┬────────┘ + │ port.postMessage({ action: 'update-macros', ... }) + ↓ +┌─────────────────┐ +│ DevTools Panel │ Triggers sidebar refresh +└─────────────────┘ +``` + +#### Flow 2: Query Macro Data (DevTools → Content Script → DevTools) + +``` +┌─────────────────┐ +│ DevTools Panel │ User selects element with -macro-dynamic-{hash} class +└────────┬────────┘ + │ port.postMessage({ type: 'query-macros', hash }) + ↓ +┌─────────────────┐ +│ Background │ Forwards to content script in specified tab +└────────┬────────┘ + │ chrome.tabs.sendMessage(tabId, { action: 'get-macro', hash }) + ↓ +┌─────────────────┐ +│ Content Script │ Checks window.__macros[hash] +│ │ • If found → responds immediately +│ │ • If not found → polls every 50ms (max 500ms) +│ │ to handle race with window.postMessage +└────────┬────────┘ + │ chrome.runtime.sendMessage({ action: 'macro-response', hash, data }) + ↓ +┌─────────────────┐ +│ Background │ Looks up DevTools connection +└────────┬────────┘ + │ port.postMessage({ action: 'macro-response', hash, data }) + ↓ +┌─────────────────┐ +│ DevTools Panel │ Resolves Promise, updates sidebar +└─────────────────┘ +``` + +#### Flow 3: Macro Cleanup (Automated) + +``` +┌─────────────────┐ +│ Content Script │ Every 5 minutes +└────────┬────────┘ + │ For each hash in window.__macros: + │ Check if document.querySelector(`.-macro-dynamic-${hash}`) exists + │ If not found, delete window.__macros[hash] + ↓ +┌─────────────────┐ +│ Garbage │ Stale macros removed from memory +│ Collection │ +└─────────────────┘ +``` + +#### Flow 4: Automatic Updates on className Changes (MutationObserver) + +When you select an element, the DevTools panel automatically watches for className changes and refreshes the panel. + +``` +┌─────────────────┐ +│ DevTools Panel │ User selects element in Elements panel +└────────┬────────┘ + │ chrome.devtools.panels.elements.onSelectionChanged + │ + │ chrome.devtools.inspectedWindow.eval(` + │ // Disconnect old observer (if any) + │ if (window.__styleMacroObserver) { + │ window.__styleMacroObserver.disconnect(); + │ } + │ + │ // Create new MutationObserver on $0 + │ window.__styleMacroObserver = new MutationObserver(() => { + │ window.postMessage({ + │ action: 'class-changed', + │ elementId: $0.getAttribute('data-devtools-id') + │ }, '*'); + │ }); + │ + │ window.__styleMacroObserver.observe($0, { + │ attributes: true, + │ attributeFilter: ['class'] + │ }); + │ `) + ↓ +┌─────────────────┐ +│ Page DOM │ MutationObserver active on selected element +└────────┬────────┘ + │ + │ ... User interacts with page, element's className changes ... + │ + │ MutationObserver detects class attribute change + │ window.postMessage({ action: 'class-changed', elementId }, '*') + ↓ +┌─────────────────┐ +│ Content Script │ Receives window message, forwards to extension +└────────┬────────┘ + │ chrome.runtime.sendMessage({ action: 'class-changed', elementId }) + ↓ +┌─────────────────┐ +│ Background │ Looks up DevTools connection for tabId +└────────┬────────┘ + │ port.postMessage({ action: 'class-changed', elementId }) + ↓ +┌─────────────────┐ +│ DevTools Panel │ Verifies elementId matches currently selected element +│ │ Triggers full panel refresh (re-reads classes, re-queries macros) +└─────────────────┘ + +When selection changes or panel closes: + ↓ +┌─────────────────┐ +│ DevTools Panel │ Calls disconnectObserver() +└────────┬────────┘ + │ chrome.devtools.inspectedWindow.eval(` + │ if (window.__styleMacroObserver) { + │ window.__styleMacroObserver.disconnect(); + │ window.__styleMacroObserver = null; + │ } + │ `) + ↓ +┌─────────────────┐ +│ Page DOM │ Old observer disconnected, new observer created for new selection +└─────────────────┘ +``` + +**Key Benefits:** +- Panel automatically refreshes when element classes change (e.g., hover states, conditional styles) +- No manual refresh needed +- Observer is cleaned up properly to prevent memory leaks +- Each element has its own unique tracking ID to prevent cross-contamination + +### Key Technical Details + +#### Why Background Script is Needed +Chrome extensions prevent direct communication between DevTools and content scripts for security reasons. The background script acts as a trusted intermediary. + +#### Static vs Dynamic Macros + +The style macro generates different class name patterns based on whether the styles can change at runtime: + +**Static Macros** (`-macro-static-{hash}`): +- Used when all style conditions are static (e.g., `style({ color: 'red' })`) +- Macro data is embedded in CSS as `--macro-data: '{...JSON...}'` custom property +- DevTools reads directly from CSS via `getComputedStyle()` + +**Dynamic Macros** (`-macro-dynamic-{hash}`): +- Used when style conditions can change (e.g., `style({ color: isActive ? 'red' : 'blue' })`) +- Macro data is sent via `window.postMessage()` whenever conditions change +- Content script stores data and responds to DevTools queries +- Enables real-time updates when props/state change + +#### Race Condition Handling (Dynamic Macros Only) +When DevTools queries for dynamic macro data, there's a race condition: +1. Page renders with `-macro-dynamic-{hash}` class name +2. DevTools sees the class and queries for the hash +3. **But**: The page's `window.postMessage` with macro data might not have reached the content script yet + +**Solution**: The content script polls `window.__macros[hash]` every 50ms for up to 500ms before giving up. This ensures the data has time to arrive via the async `window.postMessage` flow. + +Note: Static macros don't have this issue since the data is synchronously available in CSS. + +#### Connection Management +- **DevTools → Background**: Uses persistent `chrome.runtime.connect()` with port-based messaging +- **Content Script → Background**: Uses one-time `chrome.runtime.sendMessage()` calls +- **Background tracks**: Map of `tabId → DevTools port` for routing messages + +#### Data Structure +```javascript +// In window.__macros (content script context only) +{ + "zsZ9Dc": { + loc: "packages/@react-spectrum/s2/src/Button.tsx:67", + style: { + "paddingX": "4", + // ... more CSS properties + } + } +} +``` + +Note: The page context does NOT have access to `window.__macros`. This is stored only in the content script's sandboxed environment for security. + +#### Message Types + +| Message Type | Direction | Purpose | +|-------------|-----------|---------| +| `update-macros` | Page → Content → Background → DevTools | Notify that a macro was added/updated | +| `query-macros` | DevTools → Background → Content | Request macro data by hash | +| `macro-response` | Content → Background → DevTools | Return requested macro data | +| `get-macro` | Background → Content | Internal forwarding of query-macros | +| `init` | DevTools → Background | Establish connection with tabId | +| `class-changed` | Page → Content → Background → DevTools | Notify that selected element's className changed | + +### Debugging + +Enable debug logs by uncommenting the `console.log()` lines in each component: +- **DevTools Panel**: `devtool.js` → `debugLog()` function +- **Content Script**: `content-script.js` → `debugLog()` function +- **Background Script**: Already logging to service worker console + +View logs in: +- **Page Console**: Content Script and DevTools Panel logs (with `[Content Script]` and `[DevTools]` prefixes) +- **Service Worker Console**: Background Script logs (go to `chrome://extensions` → click "service worker") + diff --git a/packages/dev/style-macro-chrome-plugin/package.json b/packages/dev/style-macro-chrome-plugin/package.json new file mode 100644 index 00000000000..36f4ad189ea --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/package.json @@ -0,0 +1,25 @@ +{ + "name": "style-macro-chrome-plugin", + "version": "0.1.0", + "scripts": { + "start": "parcel watch src/manifest.json --host localhost --config .parcelrc", + "build": "parcel build src/manifest.json --config .parcelrc" + }, + "devDependencies": { + "@parcel/config-default": "^2.15.4", + "@parcel/config-webextension": "^2.15.4", + "@parcel/core": "^2.15.4", + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch", + "parcel": "^2.15.4" + }, + "rsp": { + "type": "cli" + }, + "repository": { + "type": "git", + "url": "https://github.com/adobe/react-spectrum" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/dev/style-macro-chrome-plugin/src/background.js b/packages/dev/style-macro-chrome-plugin/src/background.js new file mode 100644 index 00000000000..f91056033b0 --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/src/background.js @@ -0,0 +1,60 @@ +// Keep track of DevTools connections per tab +const devtoolsConnections = new Map(); + +// Listen for connections from DevTools +chrome.runtime.onConnect.addListener((port) => { + if (port.name === 'devtools-page') { + let tabId; + + // Listen for messages from DevTools + const messageListener = (message) => { + if (message.type === 'init') { + tabId = message.tabId; + devtoolsConnections.set(tabId, port); + console.log(`[Background] DevTools connected for tab ${tabId}`); + } else if (message.type === 'query-macros') { + console.log(`[Background] Forwarding query-macros to content script, hash: ${message.hash}, tabId: ${tabId}`); + // Forward query to content script + chrome.tabs.sendMessage(tabId, { + action: 'get-macro', + hash: message.hash + }).catch(err => { + console.error('[Background] Error sending message to content script:', err); + }); + } + }; + + port.onMessage.addListener(messageListener); + + // Clean up when DevTools disconnects + port.onDisconnect.addListener(() => { + if (tabId) { + devtoolsConnections.delete(tabId); + console.log(`DevTools disconnected for tab ${tabId}`); + } + }); + } +}); + +// Listen for messages from content scripts +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + const tabId = sender.tab?.id; + + if (!tabId) { + return; + } + + // Forward messages from content script to DevTools + if (message.action === 'update-macros' || message.action === 'macro-response' || message.action === 'class-changed') { + console.log(`[Background] Forwarding ${message.action} from content script to DevTools, tabId: ${tabId}`); + const devtoolsPort = devtoolsConnections.get(tabId); + if (devtoolsPort) { + devtoolsPort.postMessage(message); + } else { + console.warn(`[Background] No DevTools connection found for tab ${tabId}`); + } + } + + return false; // Don't keep channel open +}); + diff --git a/packages/dev/style-macro-chrome-plugin/src/content-script.js b/packages/dev/style-macro-chrome-plugin/src/content-script.js new file mode 100644 index 00000000000..e3b4626e2e6 --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/src/content-script.js @@ -0,0 +1,152 @@ + +if (window.__macrosLoaded) { + return; +} +window.__macrosLoaded = true; + +let debugLog = (...args) => { + // console.log('[Content Script]', ...args); +}; + +window.addEventListener('message', function (event) { + // Only accept messages from the same frame + if (event.source !== window) { + return; + } + + var message = event.data; + + // Only accept messages that we know are ours. Note that this is not foolproof + // and the page can easily spoof messages if it wants to. + if (message && typeof message === 'object') { + if (message.action === 'update-macros') { + let {hash, loc, style} = message; + if (!window.__macros) { + window.__macros = {}; + } + + // Update the specific macro without overwriting others + window.__macros[hash] = { + loc, + style + }; + + debugLog('Updated macro:', hash, 'Total macros:', Object.keys(window.__macros).length); + + // if this script is run multiple times on the page, then only handle it once + event.stopImmediatePropagation(); + event.stopPropagation(); + + // Send message to background script (which forwards to DevTools) + try { + chrome.runtime.sendMessage({ + action: 'update-macros', + ...message + }); + } catch (err) { + debugLog('Failed to send update-macros message:', err); + } + } else if (message.action === 'class-changed') { + // Forward class-changed messages from page context to background script + debugLog('Class changed for element:', message.elementId); + + // if this script is run multiple times on the page, then only handle it once + event.stopImmediatePropagation(); + event.stopPropagation(); + + try { + chrome.runtime.sendMessage({ + action: 'class-changed', + elementId: message.elementId + }); + } catch (err) { + debugLog('Failed to send class-changed message:', err); + } + } + } +}); + +// Listen for requests from DevTools (via background script) +chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => { + debugLog('Received message:', message); + + if (message.action === 'get-macro') { + const sendMacroResponse = (data, attempt = 1) => { + debugLog(`Sending macro-response for hash: ${message.hash}, attempt: ${attempt}, has data: ${!!data}`); + try { + chrome.runtime.sendMessage({ + action: 'macro-response', + hash: message.hash, + data: data || null + }); + } catch (err) { + debugLog('Failed to send macro-response message:', err); + } + }; + + // Check if data is immediately available + let macroData = window.__macros?.[message.hash]; + + if (macroData) { + debugLog('get-macro request for hash:', message.hash, 'Found immediately'); + sendMacroResponse(macroData, 1); + } else { + // Data not available yet, wait a bit for it to arrive via window.postMessage + debugLog('get-macro request for hash:', message.hash, 'Not found, waiting...'); + debugLog('Available macros:', window.__macros ? Object.keys(window.__macros) : 'none'); + + let attempts = 0; + const maxAttempts = 10; + const checkInterval = 50; // Check every 50ms + + const intervalId = setInterval(() => { + attempts++; + macroData = window.__macros?.[message.hash]; + + if (macroData) { + clearInterval(intervalId); + debugLog(`get-macro hash: ${message.hash} found after ${attempts} attempts (${attempts * checkInterval}ms)`); + sendMacroResponse(macroData, attempts + 1); + } else if (attempts >= maxAttempts) { + clearInterval(intervalId); + debugLog(`get-macro hash: ${message.hash} not found after ${maxAttempts} attempts, giving up`); + sendMacroResponse(null, attempts + 1); + } + }, checkInterval); + } + } +}); + +// Polling service to clean up stale macros +// Runs every 5 minutes to check if macro class names still exist on the page +const CLEANUP_INTERVAL = 1000 * 60 * 5; + +setInterval(() => { + if (!window.__macros) { + return; + } + + const macroHashes = Object.keys(window.__macros); + if (macroHashes.length === 0) { + return; + } + + let removedCount = 0; + + for (const hash of macroHashes) { + // Check if any element with this macro class exists in the DOM + const selector = `.-macro-dynamic-${CSS.escape(hash)}`; + const elementExists = document.querySelector(selector); + + if (!elementExists) { + debugLog('Cleaning up stale macro:', hash, window.__macros[hash].style); + delete window.__macros[hash]; + removedCount++; + } + } + + if (removedCount > 0) { + debugLog(`Cleaned up ${removedCount} stale macro(s). Remaining: ${Object.keys(window.__macros).length}`); + } +}, CLEANUP_INTERVAL); + diff --git a/packages/dev/style-macro-chrome-plugin/src/devtool.js b/packages/dev/style-macro-chrome-plugin/src/devtool.js new file mode 100644 index 00000000000..4d1e8f7f5ac --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/src/devtool.js @@ -0,0 +1,242 @@ + +chrome.devtools.panels.elements.createSidebarPane('Style Macros', (sidebar) => { + sidebar.setObject({}); + + // Helper function to log to both DevTools-for-DevTools console and inspected page console + const debugLog = (...args) => { + // console.log(...args); // Logs to DevTools-for-DevTools console + // const message = args.map(arg => + // typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + // ).join(' '); + // chrome.devtools.inspectedWindow.eval(`console.log('[DevTools]', ${JSON.stringify(message)})`); + }; + + const backgroundPageConnection = chrome.runtime.connect({name: 'devtools-page'}); + + // Monitor connection status + backgroundPageConnection.onDisconnect.addListener(() => { + debugLog('ERROR: Background connection disconnected!', chrome.runtime.lastError); + // Clean up observer when connection is lost + disconnectObserver(); + }); + + // Initialize connection with the background script + debugLog('Initializing connection with tabId:', chrome.devtools.inspectedWindow.tabId); + backgroundPageConnection.postMessage({ + type: 'init', + tabId: chrome.devtools.inspectedWindow.tabId + }); + debugLog('Init message sent to background'); + + // Track pending queries for macro data + const pendingQueries = new Map(); + + // Track mutation observer for selected element + let currentObserver = null; + let currentElementId = null; + + // Listen for responses from content script (via background script) + backgroundPageConnection.onMessage.addListener((message) => { + debugLog('Message from background:', message); + + if (message.action === 'macro-response') { + debugLog('Received macro-response for hash:', message.hash, 'Has data:', !!message.data); + debugLog('Pending queries has hash:', pendingQueries.has(message.hash), 'Total pending:', pendingQueries.size); + const resolve = pendingQueries.get(message.hash); + if (resolve) { + debugLog('Resolving promise for hash:', message.hash, 'with data:', message.data); + resolve(message.data); + pendingQueries.delete(message.hash); + } else { + debugLog('WARNING: No pending query found for hash:', message.hash); + } + } else if (message.action === 'update-macros') { + debugLog('Received update-macros, refreshing...'); + update(); + } else if (message.action === 'class-changed') { + debugLog('Received class-changed notification for element:', message.elementId); + // Only update if the changed element is the one we're currently watching + if (message.elementId === currentElementId) { + debugLog('Class changed on watched element, updating panel...'); + update(); + } + } + }); + + // Query macro data from content script via background script + const queryMacro = (hash) => { + debugLog('Querying macro with hash:', hash); + return new Promise((resolve) => { + pendingQueries.set(hash, resolve); + debugLog('Added to pendingQueries, total pending:', pendingQueries.size); + + try { + backgroundPageConnection.postMessage({ + type: 'query-macros', + hash: hash + }); + debugLog('Query message sent to background for hash:', hash); + } catch (err) { + debugLog('ERROR sending message:', err); + pendingQueries.delete(hash); + resolve(null); + return; + } + + // Timeout after 1 second + setTimeout(() => { + if (pendingQueries.has(hash)) { + debugLog('TIMEOUT: Query timeout for hash:', hash, 'Resolving to null'); + pendingQueries.delete(hash); + resolve(null); + } else { + debugLog('Timeout fired for hash:', hash, 'but query already resolved'); + } + }, 1000); + }); + }; + + function getMacroData(className) { + let promise = new Promise((resolve) => { + debugLog('Getting macro data for:', className); + chrome.devtools.inspectedWindow.eval('window.getComputedStyle($0).getPropertyValue("--macro-data")', (style) => { + debugLog('Got style:', style); + resolve(style ? JSON.parse(style) : null); + }); + }); + return promise; + } + + // Function to disconnect the current observer + const disconnectObserver = () => { + if (currentObserver) { + chrome.devtools.inspectedWindow.eval(` + if (window.__styleMacroObserver) { + window.__styleMacroObserver.disconnect(); + window.__styleMacroObserver = null; + } + `); + debugLog('Disconnected mutation observer for element:', currentElementId); + currentObserver = null; + currentElementId = null; + } + }; + + // Function to start observing the currently selected element + const startObserving = () => { + // First disconnect any existing observer + disconnectObserver(); + + // Generate a unique ID for the current element + chrome.devtools.inspectedWindow.eval(` + (function() { + const element = $0; + if (!element || !element.classList) { + return null; + } + + // Generate a unique ID if element doesn't have one + if (!element.hasAttribute('data-devtools-id')) { + element.setAttribute('data-devtools-id', 'dt-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9)); + } + + const elementId = element.getAttribute('data-devtools-id'); + + // Create mutation observer + if (window.__styleMacroObserver) { + window.__styleMacroObserver.disconnect(); + } + + window.__styleMacroObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + // Notify DevTools that the class has changed via window.postMessage + // (chrome.runtime is not available in page context) + window.postMessage({ + action: 'class-changed', + elementId: elementId + }, '*'); + break; + } + } + }); + + window.__styleMacroObserver.observe(element, { + attributes: true, + attributeFilter: ['class'] + }); + + return elementId; + })(); + `, (result, isException) => { + if (isException) { + debugLog('Error setting up mutation observer:', result); + } else if (result) { + currentElementId = result; + currentObserver = true; // Just track that we have an observer + debugLog('Started observing element:', currentElementId); + } + }); + }; + + let update = () => { + debugLog('Starting update...'); + chrome.devtools.inspectedWindow.eval('$0.getAttribute("class")', (className) => { + debugLog('Got className:', className); + + // Handle the async operations outside the eval callback + (async () => { + if (typeof className !== 'string') { + sidebar.setObject({}); + return; + } + + let staticMacroHashes = [...className.matchAll(/-macro-static-([^\s]+)/g)].map(m => m[1]); + let dynamicMacroHashes = [...className.matchAll(/-macro-dynamic-([^\s]+)/g)].map(m => m[1]); + debugLog('Static macro hashes:', staticMacroHashes); + debugLog('Dynamic macro hashes:', dynamicMacroHashes); + + let staticMacros = staticMacroHashes.map(macro => getMacroData(macro)); + let dynamicMacros = dynamicMacroHashes.map(macro => queryMacro(macro)); + + debugLog('Waiting for', staticMacros.length, 'static and', dynamicMacros.length, 'dynamic macros...'); + let results = await Promise.all([...staticMacros, ...dynamicMacros]); + debugLog('Results:', results); + + if (results.length === 0) { + sidebar.setObject({}); + } else if (results.length === 1) { + sidebar.setObject(results[0].style ?? {}, results[0].loc); + } else { + let seenProperties = new Set(); + for (let i = results.length - 1; i >= 0; i--) { + for (let key in results[i].style) { + if (seenProperties.has(key)) { + delete results[i].style[key]; + } else { + seenProperties.add(key); + } + } + } + + let res = {}; + for (let result of results) { + res[result.loc] = result.style; + } + sidebar.setObject(res); + } + })(); + }); + }; + + chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { + debugLog('Element selection changed'); + // Start observing the newly selected element + startObserving(); + // Update the panel with the new element's macros + update(); + }); + + // Initial observation when the panel is first opened + startObserving(); +}); diff --git a/packages/dev/style-macro-chrome-plugin/src/devtools.html b/packages/dev/style-macro-chrome-plugin/src/devtools.html new file mode 100644 index 00000000000..6f5f400e09b --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/src/devtools.html @@ -0,0 +1,7 @@ + + + + Devtools! + + + diff --git a/packages/dev/style-macro-chrome-plugin/src/icons/128.png b/packages/dev/style-macro-chrome-plugin/src/icons/128.png new file mode 100644 index 00000000000..a0a1377e1a9 Binary files /dev/null and b/packages/dev/style-macro-chrome-plugin/src/icons/128.png differ diff --git a/packages/dev/style-macro-chrome-plugin/src/icons/16.png b/packages/dev/style-macro-chrome-plugin/src/icons/16.png new file mode 100644 index 00000000000..41a22d0a311 Binary files /dev/null and b/packages/dev/style-macro-chrome-plugin/src/icons/16.png differ diff --git a/packages/dev/style-macro-chrome-plugin/src/icons/32.png b/packages/dev/style-macro-chrome-plugin/src/icons/32.png new file mode 100644 index 00000000000..493110735d2 Binary files /dev/null and b/packages/dev/style-macro-chrome-plugin/src/icons/32.png differ diff --git a/packages/dev/style-macro-chrome-plugin/src/icons/48.png b/packages/dev/style-macro-chrome-plugin/src/icons/48.png new file mode 100644 index 00000000000..6794ef2da83 Binary files /dev/null and b/packages/dev/style-macro-chrome-plugin/src/icons/48.png differ diff --git a/packages/dev/style-macro-chrome-plugin/src/icons/96.png b/packages/dev/style-macro-chrome-plugin/src/icons/96.png new file mode 100644 index 00000000000..b12d2c4a2f0 Binary files /dev/null and b/packages/dev/style-macro-chrome-plugin/src/icons/96.png differ diff --git a/packages/dev/style-macro-chrome-plugin/src/manifest.json b/packages/dev/style-macro-chrome-plugin/src/manifest.json new file mode 100644 index 00000000000..6d3bfaf858a --- /dev/null +++ b/packages/dev/style-macro-chrome-plugin/src/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "Style Macro DevTools", + "version": "0.0.1", + "devtools_page": "devtools.html", + "background": { + "service_worker": "background.js" + }, + "content_scripts": [{ + "matches": ["*://*/*"], + "js": ["content-script.js"], + "all_frames": true, + "run_at": "document_idle" + }], + "permissions": ["tabs"], + "icons": { + "16": "icons/16.png", + "32": "icons/32.png", + "48": "icons/48.png", + "96": "icons/96.png", + "128": "icons/128.png" + } +} diff --git a/yarn.lock b/yarn.lock index dbf1c445a80..5b58e949ce0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4325,7 +4325,7 @@ __metadata: languageName: node linkType: hard -"@parcel/config-default@npm:2.16.0, @parcel/config-default@npm:^2.16.0": +"@parcel/config-default@npm:2.16.0, @parcel/config-default@npm:^2.15.4, @parcel/config-default@npm:^2.16.0": version: 2.16.0 resolution: "@parcel/config-default@npm:2.16.0" dependencies: @@ -4374,13 +4374,28 @@ __metadata: "@parcel/config-default": "npm:^2.16.0" "@parcel/core": "npm:^2.16.0" "@parcel/resolver-storybook": "npm:>=0.0.0" - "@parcel/transformer-js": "npm:^2.16.0" + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch" "@parcel/transformer-react-refresh-wrap": "npm:^2.16.0" "@parcel/transformer-storybook": "npm:>=0.0.2" languageName: unknown linkType: soft -"@parcel/core@npm:2.16.0, @parcel/core@npm:^2.16.0": +"@parcel/config-webextension@npm:^2.15.4": + version: 2.16.0 + resolution: "@parcel/config-webextension@npm:2.16.0" + dependencies: + "@parcel/config-default": "npm:2.16.0" + "@parcel/packager-webextension": "npm:2.16.0" + "@parcel/runtime-webextension": "npm:2.16.0" + "@parcel/transformer-raw": "npm:2.16.0" + "@parcel/transformer-webextension": "npm:2.16.0" + peerDependencies: + "@parcel/core": ^2.16.0 + checksum: 10c0/f00c3bac7487cb7dcfe5cd0564464daec09dc20b4aae225bc3868377a1aa1f6ade7763593380bd078e4d6711fb7d7163542945671ea914129387e176bb8b3af9 + languageName: node + linkType: hard + +"@parcel/core@npm:2.16.0, @parcel/core@npm:^2.15.4, @parcel/core@npm:^2.16.0": version: 2.16.0 resolution: "@parcel/core@npm:2.16.0" dependencies: @@ -4729,6 +4744,17 @@ __metadata: languageName: node linkType: hard +"@parcel/packager-webextension@npm:2.16.0": + version: 2.16.0 + resolution: "@parcel/packager-webextension@npm:2.16.0" + dependencies: + "@parcel/plugin": "npm:2.16.0" + "@parcel/utils": "npm:2.16.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/654c917752e661cdf0cb14cb9a3ebdbd8105a46e203f6f0c0ee516f875f9492b24b7d34962f0961c1ba419557e3db06a6333d6de4990a254008bd96b108ea6e8 + languageName: node + linkType: hard + "@parcel/plugin@npm:2.16.0, @parcel/plugin@npm:^2.0.0, @parcel/plugin@npm:^2.16.0": version: 2.16.0 resolution: "@parcel/plugin@npm:2.16.0" @@ -4890,6 +4916,17 @@ __metadata: languageName: node linkType: hard +"@parcel/runtime-webextension@npm:2.16.0": + version: 2.16.0 + resolution: "@parcel/runtime-webextension@npm:2.16.0" + dependencies: + "@parcel/plugin": "npm:2.16.0" + "@parcel/utils": "npm:2.16.0" + nullthrows: "npm:^1.1.1" + checksum: 10c0/69d29e3b4b415222c0ef4816a018d65470c623e6cb72a3e9c3d0882bd0c022c89b0c9dd641509a2326792d2d69472839a529c259cb61da97ea546d262fca3130 + languageName: node + linkType: hard + "@parcel/rust-darwin-arm64@npm:2.16.0": version: 2.16.0 resolution: "@parcel/rust-darwin-arm64@npm:2.16.0" @@ -5067,7 +5104,7 @@ __metadata: languageName: node linkType: hard -"@parcel/transformer-js@npm:2.16.0, @parcel/transformer-js@npm:^2.16.0": +"@parcel/transformer-js@npm:2.16.0": version: 2.16.0 resolution: "@parcel/transformer-js@npm:2.16.0" dependencies: @@ -5088,6 +5125,27 @@ __metadata: languageName: node linkType: hard +"@parcel/transformer-js@patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch": + version: 2.16.0 + resolution: "@parcel/transformer-js@patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch::version=2.16.0&hash=6257d2" + dependencies: + "@parcel/diagnostic": "npm:2.16.0" + "@parcel/plugin": "npm:2.16.0" + "@parcel/rust": "npm:2.16.0" + "@parcel/source-map": "npm:^2.1.1" + "@parcel/utils": "npm:2.16.0" + "@parcel/workers": "npm:2.16.0" + "@swc/helpers": "npm:^0.5.0" + browserslist: "npm:^4.24.5" + nullthrows: "npm:^1.1.1" + regenerator-runtime: "npm:^0.14.1" + semver: "npm:^7.7.1" + peerDependencies: + "@parcel/core": ^2.16.0 + checksum: 10c0/71ab8284ce896b33e3fbfb54bd8da3c16c50a0692c26b24e1a70cfc748751ee9a608910015e86dc61bc023cf910f72426ba131035c5c022dda0c6c3ad866cbab + languageName: node + linkType: hard + "@parcel/transformer-json@npm:2.16.0": version: 2.16.0 resolution: "@parcel/transformer-json@npm:2.16.0" @@ -5219,6 +5277,19 @@ __metadata: languageName: node linkType: hard +"@parcel/transformer-webextension@npm:2.16.0": + version: 2.16.0 + resolution: "@parcel/transformer-webextension@npm:2.16.0" + dependencies: + "@mischnic/json-sourcemap": "npm:^0.1.1" + "@parcel/diagnostic": "npm:2.16.0" + "@parcel/plugin": "npm:2.16.0" + "@parcel/utils": "npm:2.16.0" + content-security-policy-parser: "npm:^0.6.0" + checksum: 10c0/d0df12b746354d887f0fdf45924efb4155c839b85cdcb08ffc7ef56c569d8c119bbe7d1783f2a9d83ae748b5d34f4263ae0fa51aa2a1978af0a6f2a5da85571f + languageName: node + linkType: hard + "@parcel/ts-utils@npm:2.16.0": version: 2.16.0 resolution: "@parcel/ts-utils@npm:2.16.0" @@ -7503,7 +7574,7 @@ __metadata: "@parcel/plugin": "npm:^2.16.0" "@parcel/reporter-cli": "npm:^2.16.0" "@parcel/resolver-default": "npm:^2.16.0" - "@parcel/transformer-js": "npm:^2.16.0" + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch" "@parcel/transformer-raw": "npm:^2.16.0" "@parcel/transformer-react-refresh-wrap": "npm:^2.16.0" "@react-spectrum/parcel-namer-s2": "npm:^0.3.2" @@ -13664,6 +13735,13 @@ __metadata: languageName: node linkType: hard +"content-security-policy-parser@npm:^0.6.0": + version: 0.6.0 + resolution: "content-security-policy-parser@npm:0.6.0" + checksum: 10c0/f494e69020e2320179eab47ad2cdafb09752ed63ca4fb5445071381e392d19edd110c0c3ec43f135d27c34b49dbab851b7fcf188dd2ba30cacd6e1107b15b674 + languageName: node + linkType: hard + "content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" @@ -23363,7 +23441,7 @@ __metadata: languageName: unknown linkType: soft -"parcel@npm:^2.16.0": +"parcel@npm:^2.15.4, parcel@npm:^2.16.0": version: 2.16.0 resolution: "parcel@npm:2.16.0" dependencies: @@ -27071,6 +27149,18 @@ __metadata: languageName: node linkType: hard +"style-macro-chrome-plugin@workspace:packages/dev/style-macro-chrome-plugin": + version: 0.0.0-use.local + resolution: "style-macro-chrome-plugin@workspace:packages/dev/style-macro-chrome-plugin" + dependencies: + "@parcel/config-default": "npm:^2.15.4" + "@parcel/config-webextension": "npm:^2.15.4" + "@parcel/core": "npm:^2.15.4" + "@parcel/transformer-js": "patch:@parcel/transformer-js@npm%3A2.16.0#~/.yarn/patches/@parcel-transformer-js-npm-2.16.0-ae71b060cb.patch" + parcel: "npm:^2.15.4" + languageName: unknown + linkType: soft + "style-to-object@npm:^0.3.0": version: 0.3.0 resolution: "style-to-object@npm:0.3.0"