Skip to content

Commit

Permalink
fix(extractor): JSX nodes as JSX attributes (#1792)
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer authored Dec 8, 2023
1 parent 175af58 commit 1464460
Show file tree
Hide file tree
Showing 25 changed files with 237 additions and 70 deletions.
25 changes: 25 additions & 0 deletions .changeset/brave-cars-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@pandacss/extractor': patch
---

Fix static extraction issue when using JSX attributes (props) that are other JSX nodes

While parsing over the AST Nodes, due to an optimization where we skipped retrieving the current JSX element and instead
kept track of the latest one, the logic was flawed and did not extract other properties after encountering a JSX
attribute that was another JSX node.

```tsx
const Component = () => {
return (
<>
{/* ❌ this wasn't extracting ml="2" */}
<Flex icon={<svg className="icon" />} ml="2" />

{/* ✅ this was fine */}
<Stack ml="4" icon={<div className="icon" />} />
</>
)
}
```

Now both will be fine again.
80 changes: 79 additions & 1 deletion packages/extractor/__tests__/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const config: Record<string, string[]> = {

const componentsMatcher: ComponentMatchers = {
matchTag: ({ tagName }) => Boolean(config[tagName]),
matchProp: ({ tagName, propName }) => config[tagName].includes(propName),
matchProp: ({ tagName, propName }) => config[tagName]?.includes(propName),
}

type TestExtractOptions = Omit<ExtractOptions, 'ast'> & { tagNameList?: string[]; functionNameList?: string[] }
Expand Down Expand Up @@ -5795,6 +5795,84 @@ it('extracts arrays without removing nullish values', () => {
`)
})

it('extracts props after a JSX attribute containing a JSX element', () => {
expect(
extractFromCode(
`
export const App = () => {
return (
<>
<Flex icon={<svg />} ml="2" />
<Stack ml="4" icon={<div />} />
</>
);
};
`,
{ tagNameList: ['Flex', 'Stack'] },
),
).toMatchInlineSnapshot(`
{
"Flex": [
{
"conditions": [],
"raw": {
"ml": "2",
},
"spreadConditions": [],
},
],
"Stack": [
{
"conditions": [],
"raw": {
"ml": "4",
},
"spreadConditions": [],
},
],
}
`)
})

it('extracts props after a JSX spread containing a JSX element', () => {
expect(
extractFromCode(
`
export const App = () => {
return (
<>
<Flex {...{ icon: <svg /> }} ml="2" />
<Stack ml="4" {...{ icon: <div /> }} />
</>
);
};
`,
{ tagNameList: ['Flex', 'Stack'] },
),
).toMatchInlineSnapshot(`
{
"Flex": [
{
"conditions": [],
"raw": {
"ml": "2",
},
"spreadConditions": [],
},
],
"Stack": [
{
"conditions": [],
"raw": {
"ml": "4",
},
"spreadConditions": [],
},
],
}
`)
})

it('handles TS enum', () => {
const code = `import { sva } from 'styled-system/css';
Expand Down
32 changes: 16 additions & 16 deletions packages/extractor/src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface Component {
type ComponentMap = Map<JsxElement, Component>

const isImportOrExport = (node: Node) => Node.isImportDeclaration(node) || Node.isExportDeclaration(node)
const isJsxElement = (node: Node) => Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)

export const extract = ({ ast, ...ctx }: ExtractOptions) => {
const { components, functions, taggedTemplates } = ctx
Expand All @@ -43,14 +44,6 @@ export const extract = ({ ast, ...ctx }: ExtractOptions) => {
*/
const componentByNode: ComponentMap = new Map()

// keep track of the current component node
// so we don't have to traverse the tree upwards again
let componentNode: JsxElement | undefined
let componentName: string
let isFactory: boolean
let boxByProp: ExtractedComponentResult['nodesByProp']
let component: Component

ast.forEachDescendant((node, traversal) => {
// quick win
if (isImportOrExport(node)) {
Expand All @@ -60,34 +53,35 @@ export const extract = ({ ast, ...ctx }: ExtractOptions) => {

if (components) {
if (Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)) {
componentNode = node
componentName = getComponentName(componentNode)
isFactory = componentName.includes('.')
const componentNode = node
const componentName = getComponentName(componentNode)
const isFactory = componentName.includes('.')

if (!components.matchTag({ tagNode: componentNode, tagName: componentName, isFactory })) {
componentNode = undefined
return
}

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

boxByProp = byName.get(componentName)!.nodesByProp

if (!componentByNode.has(componentNode)) {
componentByNode.set(componentNode, { name: componentName, props: new Map(), conditionals: [] })
}

component = componentByNode.get(componentNode)!
}

if (Node.isJsxSpreadAttribute(node)) {
const componentNode = node.getFirstAncestor(isJsxElement) as JsxElement
const component = componentByNode.get(componentNode)

// <ColorBox {...{ color: "facebook.100" }}>spread</ColorBox>
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

if (!componentNode || !component) return

const componentName = getComponentName(componentNode)
const boxByProp = byName.get(componentName)!.nodesByProp

const matchProp = ({ propName, propNode }: MatchPropArgs) =>
components.matchProp({ tagNode: componentNode!, tagName: componentName, propName, propNode })

Expand Down Expand Up @@ -136,8 +130,14 @@ export const extract = ({ ast, ...ctx }: ExtractOptions) => {
// <ColorBox color="red.200" backgroundColor="blackAlpha.100" />
// ^^^^^ ^^^^^^^^^^^^^^^

const componentNode = node.getFirstAncestor(isJsxElement) as JsxElement
const component = componentByNode.get(componentNode)

if (!componentNode || !component) return

const componentName = getComponentName(componentNode)
const boxByProp = byName.get(componentName)!.nodesByProp

const propName = node.getNameNode().getText()
if (!components.matchProp({ tagNode: componentNode, tagName: componentName, propName, propNode: node })) {
return
Expand Down
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/aspect-ratio.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getAspectRatioStyle } from '../patterns/aspect-ratio.mjs';
export const AspectRatio = /* @__PURE__ */ forwardRef(function AspectRatio(props, ref) {
const { ratio, ...restProps } = props
const styleProps = getAspectRatioStyle({ratio})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/bleed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getBleedStyle } from '../patterns/bleed.mjs';
export const Bleed = /* @__PURE__ */ forwardRef(function Bleed(props, ref) {
const { inline, block, ...restProps } = props
const styleProps = getBleedStyle({inline, block})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
10 changes: 7 additions & 3 deletions packages/studio/styled-system/jsx/box.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { panda } from './factory.mjs';
import { getBoxStyle } from '../patterns/box.mjs';

export const Box = /* @__PURE__ */ forwardRef(function Box(props, ref) {
const styleProps = getBoxStyle()
return createElement(panda.div, { ref, ...styleProps, ...props })
})
const restProps = props
const styleProps = getBoxStyle()
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/center.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getCenterStyle } from '../patterns/center.mjs';
export const Center = /* @__PURE__ */ forwardRef(function Center(props, ref) {
const { inline, ...restProps } = props
const styleProps = getCenterStyle({inline})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/circle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getCircleStyle } from '../patterns/circle.mjs';
export const Circle = /* @__PURE__ */ forwardRef(function Circle(props, ref) {
const { size, ...restProps } = props
const styleProps = getCircleStyle({size})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
10 changes: 7 additions & 3 deletions packages/studio/styled-system/jsx/container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { panda } from './factory.mjs';
import { getContainerStyle } from '../patterns/container.mjs';

export const Container = /* @__PURE__ */ forwardRef(function Container(props, ref) {
const styleProps = getContainerStyle()
return createElement(panda.div, { ref, ...styleProps, ...props })
})
const restProps = props
const styleProps = getContainerStyle()
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/divider.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getDividerStyle } from '../patterns/divider.mjs';
export const Divider = /* @__PURE__ */ forwardRef(function Divider(props, ref) {
const { orientation, thickness, color, ...restProps } = props
const styleProps = getDividerStyle({orientation, thickness, color})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/flex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getFlexStyle } from '../patterns/flex.mjs';
export const Flex = /* @__PURE__ */ forwardRef(function Flex(props, ref) {
const { align, justify, direction, wrap, basis, grow, shrink, ...restProps } = props
const styleProps = getFlexStyle({align, justify, direction, wrap, basis, grow, shrink})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/float.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getFloatStyle } from '../patterns/float.mjs';
export const Float = /* @__PURE__ */ forwardRef(function Float(props, ref) {
const { offsetX, offsetY, offset, placement, ...restProps } = props
const styleProps = getFloatStyle({offsetX, offsetY, offset, placement})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/grid-item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getGridItemStyle } from '../patterns/grid-item.mjs';
export const GridItem = /* @__PURE__ */ forwardRef(function GridItem(props, ref) {
const { colSpan, rowSpan, colStart, rowStart, colEnd, rowEnd, ...restProps } = props
const styleProps = getGridItemStyle({colSpan, rowSpan, colStart, rowStart, colEnd, rowEnd})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/grid.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getGridStyle } from '../patterns/grid.mjs';
export const Grid = /* @__PURE__ */ forwardRef(function Grid(props, ref) {
const { gap, columnGap, rowGap, columns, minChildWidth, ...restProps } = props
const styleProps = getGridStyle({gap, columnGap, rowGap, columns, minChildWidth})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
7 changes: 5 additions & 2 deletions packages/studio/styled-system/jsx/hstack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ import { getHstackStyle } from '../patterns/hstack.mjs';
export const HStack = /* @__PURE__ */ forwardRef(function HStack(props, ref) {
const { justify, gap, ...restProps } = props
const styleProps = getHstackStyle({justify, gap})
return createElement(panda.div, { ref, ...styleProps, ...restProps })
})
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
10 changes: 7 additions & 3 deletions packages/studio/styled-system/jsx/link-box.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { panda } from './factory.mjs';
import { getLinkBoxStyle } from '../patterns/link-box.mjs';

export const LinkBox = /* @__PURE__ */ forwardRef(function LinkBox(props, ref) {
const styleProps = getLinkBoxStyle()
return createElement(panda.div, { ref, ...styleProps, ...props })
})
const restProps = props
const styleProps = getLinkBoxStyle()
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.div, mergedProps)
})
10 changes: 7 additions & 3 deletions packages/studio/styled-system/jsx/link-overlay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { panda } from './factory.mjs';
import { getLinkOverlayStyle } from '../patterns/link-overlay.mjs';

export const LinkOverlay = /* @__PURE__ */ forwardRef(function LinkOverlay(props, ref) {
const styleProps = getLinkOverlayStyle()
return createElement(panda.a, { ref, ...styleProps, ...props })
})
const restProps = props
const styleProps = getLinkOverlayStyle()
const cssProps = styleProps
const mergedProps = { ref, ...cssProps, ...restProps }

return createElement(panda.a, mergedProps)
})
Loading

3 comments on commit 1464460

@vercel
Copy link

@vercel vercel bot commented on 1464460 Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

panda-docs – ./website

panda-docs.vercel.app
panda-docs-chakra-ui.vercel.app
panda-css.com
panda-docs-git-main-chakra-ui.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1464460 Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 1464460 Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

panda-studio – ./

panda-studio-git-main-chakra-ui.vercel.app
panda-app.vercel.app
panda-studio-chakra-ui.vercel.app

Please sign in to comment.