Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/extract-react-jsx-runtime-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@pandacss/extractor': patch
'@pandacss/parser': patch
---

Recognize React automatic-runtime JSX calls (`jsx`, `jsxs`, `jsxDEV`) as component
instances so Panda can extract styles from already-compiled files.

`<Box css={{ color: 'red' }} />` compiles down to `jsx(Box, { css: { color: 'red' } })`
via the React automatic JSX runtime. The extractor now treats that call as a synthetic
JSX element, which is what lets Panda scan pre-compiled `dist` bundles shipped by
component libraries and still produce CSS for inline object literals on the `css` prop.

Closes #3509.
86 changes: 84 additions & 2 deletions packages/extractor/src/extract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsxOpeningElement, JsxSelfClosingElement, Node } from 'ts-morph'
import { CallExpression, JsxOpeningElement, JsxSelfClosingElement, Node } from 'ts-morph'
import { box } from './box'
import { BoxNodeMap, BoxNodeObject, type BoxNode, type MapTypeValue, BoxNodeConditional } from './box-factory'
import { extractCallExpressionArguments } from './call-expression'
Expand All @@ -16,9 +16,14 @@ import type {
MatchFnPropArgs,
MatchPropArgs,
} from './types'
import { getComponentName } from './utils'
import { getComponentName, unwrapExpression } from './utils'
import { maybeBoxNode } from './maybe-box-node'

// Names of the React automatic JSX runtime helpers (`react/jsx-runtime` and `react/jsx-dev-runtime`).
// A call like `jsx(Box, { css: { ... } })` is the compiled form of `<Box css={{ ... }} />` and must
// be extracted the same way so that Panda can scan already-compiled files (e.g. a library's dist output).
const REACT_JSX_RUNTIME_FNS = new Set(['jsx', 'jsxs', 'jsxDEV'])

type JsxElement = JsxOpeningElement | JsxSelfClosingElement
interface Component {
name: string
Expand All @@ -44,6 +49,72 @@ export const extract = ({ ast, ...ctx }: ExtractOptions) => {
*/
const componentByNode: ComponentMap = new Map()

// Handles a React automatic-runtime JSX call (`jsx`/`jsxs`/`jsxDEV`) as a synthetic component instance.
// `components` is captured from the enclosing scope and is guaranteed non-null at the call site.
const extractJsxRuntimeCall = (node: CallExpression) => {
if (!components) return

const args = node.getArguments()
if (args.length < 2) return

const tagNode = unwrapExpression(args[0])
const tagName = Node.isStringLiteral(tagNode) ? tagNode.getLiteralValue() : tagNode.getText()
const isFactory = tagName.includes('.')

// Passing the CallExpression as `tagNode` is a deliberate shape-compatibility choice:
// downstream matchers use it only for ancestry and identity, not for JSX-specific APIs.
if (!components.matchTag({ tagNode: node as any, tagName, isFactory })) return

const propsArg = unwrapExpression(args[1])
if (!Node.isObjectLiteralExpression(propsArg)) return

if (!byName.has(tagName)) {
byName.set(tagName, { kind: 'component', nodesByProp: new Map(), queryList: [] })
}

const componentResult = byName.get(tagName) as ExtractedComponentResult
const componentBoxByProp = componentResult.nodesByProp
const props: MapTypeValue = new Map()

const matchProp = ({ propName, propNode }: MatchPropArgs) =>
components.matchProp({ tagNode: node as any, tagName, propName, propNode })

for (const property of propsArg.getProperties()) {
if (Node.isPropertyAssignment(property)) {
const propName = property.getName()
if (!matchProp({ propName, propNode: property as any })) continue

const initializer = property.getInitializer()
if (!initializer) continue

const stack: Node[] = [node, propsArg, property, initializer]
const boxNode = maybeBoxNode(unwrapExpression(initializer), stack, ctx)
if (!boxNode) continue

props.set(propName, boxNode)
componentBoxByProp.set(propName, (componentBoxByProp.get(propName) ?? []).concat(boxNode))
} else if (Node.isShorthandPropertyAssignment(property)) {
const propName = property.getName()
if (!matchProp({ propName, propNode: property as any })) continue

const nameNode = property.getNameNode()
const stack: Node[] = [node, propsArg, property]
const boxNode = maybeBoxNode(nameNode, stack, ctx)
if (!boxNode) continue

props.set(propName, boxNode)
componentBoxByProp.set(propName, (componentBoxByProp.get(propName) ?? []).concat(boxNode))
}
// SpreadAssignment intentionally left unhandled for v1 β€” keeps the change narrow.
}

const instance = {
name: tagName,
box: box.map(props, node, []),
} as ExtractedComponentInstance
componentResult.queryList.push(instance)
}

ast.forEachDescendant((node, traversal) => {
// quick win
if (isImportOrExport(node)) {
Expand Down Expand Up @@ -149,6 +220,17 @@ export const extract = ({ ast, ...ctx }: ExtractOptions) => {
component.props.set(propName, maybeBox)
boxByProp.set(propName, (boxByProp.get(propName) ?? []).concat(maybeBox))
}

if (Node.isCallExpression(node) && REACT_JSX_RUNTIME_FNS.has(node.getExpression().getText())) {
// jsx(Box, { css: { color: 'red' } })
// jsxs(Box, { ... })
// jsxDEV(Box, { ... })
//
// React's automatic JSX runtime compiles `<Box css={{ ... }} />` into one of the calls above.
// We extract such calls as component instances so that scanning pre-compiled code
// (e.g. a component library's published `dist` bundle) still yields the expected CSS.
extractJsxRuntimeCall(node)
}
}

if (functions && Node.isCallExpression(node)) {
Expand Down
29 changes: 29 additions & 0 deletions packages/parser/__tests__/css-prop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,33 @@ describe('ast parser / css prop', () => {
}"
`)
})

// Regression test for https://github.com/chakra-ui/panda/issues/3509
// React's automatic JSX runtime compiles `<Test css={{ bg: 'red.200' }} />`
// down to `jsx(Test, { css: { bg: 'red.200' } })`. Panda's extractor should
// recognize the inline object literal passed to the `css` prop in that form
// so that scanning compiled `dist` files of component libraries still produces CSS.
test('should parse compiled jsx() call form with inline css object literal', () => {
const code = `
import { jsx } from "react/jsx-runtime"
import { css, cx } from "styled-system/css"

const Test = (props) => {
const { css: cssProp, children } = props
return jsx("div", { className: cx(css(cssProp)), children })
}

const test = jsx(Test, { css: { bg: "red.200" } })
`

const result = parseAndExtract(code)

expect(result.css).toMatchInlineSnapshot(`
"@layer utilities {
.bg_red\\.200 {
background: var(--colors-red-200);
}
}"
`)
})
})