Skip to content

Commit 27e872a

Browse files
committed
[pigment] Handle more scenarios while transforming
sx prop. This now provides the ability to have simple expressions like conditional and logical expressions in the sx prop code.
1 parent ad6c50f commit 27e872a

File tree

11 files changed

+249
-109
lines changed

11 files changed

+249
-109
lines changed

packages/pigment-react/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
"stylis": "^4.3.1"
5151
},
5252
"devDependencies": {
53+
"@babel/plugin-syntax-jsx": "^7.23.3",
5354
"@types/babel__core": "^7.20.5",
5455
"@types/babel__helper-module-imports": "^7.18.3",
5556
"@types/babel__helper-plugin-utils": "^7.10.3",
5657
"@types/cssesc": "^3.0.2",
5758
"@types/lodash": "^4.14.202",
59+
"@types/mocha": "^10.0.6",
5860
"@types/node": "^18.19.21",
5961
"@types/react": "^18.2.55",
6062
"@types/stylis": "^4.2.5",
@@ -130,15 +132,6 @@
130132
}
131133
},
132134
"nx": {
133-
"targetDefaults": {
134-
"build": {
135-
"outputs": [
136-
"{projectRoot}/build",
137-
"{projectRoot}/processors",
138-
"{projectRoot}/utils"
139-
]
140-
}
141-
},
142135
"targets": {
143136
"test": {
144137
"cache": false,
@@ -151,6 +144,13 @@
151144
"dependsOn": [
152145
"build"
153146
]
147+
},
148+
"build": {
149+
"outputs": [
150+
"{projectRoot}/build",
151+
"{projectRoot}/processors",
152+
"{projectRoot}/utils"
153+
]
154154
}
155155
}
156156
}

packages/pigment-react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { default as keyframes } from './keyframes';
44
export { generateAtomics, atomics } from './generateAtomics';
55
export { default as css } from './css';
66
export { default as createUseThemeProps } from './createUseThemeProps';
7+
export { clsx } from 'clsx';
Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,83 @@
11
import { addNamed } from '@babel/helper-module-imports';
22
import { declare } from '@babel/helper-plugin-utils';
3-
import { sxObjectExtractor } from './sxObjectExtractor';
3+
import { NodePath } from '@babel/core';
4+
import * as Types from '@babel/types';
5+
import { sxPropConvertor } from './sxPropConverter';
46

5-
export const babelPlugin = declare((api) => {
6-
api.assertVersion(7);
7-
const { types: t } = api;
8-
return {
9-
name: '@pigmentcss/zero-babel-plugin',
10-
visitor: {
11-
JSXAttribute(path) {
12-
const namePath = path.get('name');
13-
const openingElement = path.findParent((p) => p.isJSXOpeningElement());
14-
if (
15-
!openingElement ||
16-
!openingElement.isJSXOpeningElement() ||
17-
!namePath.isJSXIdentifier() ||
18-
namePath.node.name !== 'sx'
19-
) {
20-
return;
21-
}
22-
const tagName = openingElement.get('name');
23-
if (!tagName.isJSXIdentifier()) {
24-
return;
25-
}
26-
const valuePath = path.get('value');
27-
if (!valuePath.isJSXExpressionContainer()) {
28-
return;
29-
}
30-
const expressionPath = valuePath.get('expression');
31-
if (!expressionPath.isExpression()) {
32-
return;
33-
}
34-
if (!expressionPath.isObjectExpression() && !expressionPath.isArrowFunctionExpression()) {
35-
return;
36-
}
37-
sxObjectExtractor(expressionPath);
38-
const sxIdentifier = addNamed(namePath, 'sx', process.env.PACKAGE_NAME as string);
39-
expressionPath.replaceWith(
40-
t.callExpression(sxIdentifier, [expressionPath.node, t.identifier(tagName.node.name)]),
41-
);
42-
},
43-
ObjectProperty(path) {
44-
// @TODO - Maybe add support for React.createElement calls as well.
45-
// Right now, it only checks for jsx(),jsxs(),jsxDEV() and jsxsDEV() calls.
46-
const keyPath = path.get('key');
47-
if (!keyPath.isIdentifier() || keyPath.node.name !== 'sx') {
48-
return;
49-
}
50-
const valuePath = path.get('value');
51-
if (!valuePath.isObjectExpression() && !valuePath.isArrowFunctionExpression()) {
52-
return;
53-
}
54-
const parentJsxCall = path.findParent((p) => p.isCallExpression());
55-
if (!parentJsxCall || !parentJsxCall.isCallExpression()) {
56-
return;
57-
}
58-
const callee = parentJsxCall.get('callee');
59-
if (!callee.isIdentifier() || !callee.node.name.includes('jsx')) {
60-
return;
61-
}
62-
const jsxElement = parentJsxCall.get('arguments')[0];
63-
sxObjectExtractor(valuePath);
64-
const sxIdentifier = addNamed(keyPath, 'sx', process.env.PACKAGE_NAME as string);
65-
valuePath.replaceWith(t.callExpression(sxIdentifier, [valuePath.node, jsxElement.node]));
66-
},
67-
},
7+
function replaceNodePath(
8+
expressionPath: NodePath<Types.Expression>,
9+
namePath: NodePath<Types.JSXIdentifier | Types.Identifier>,
10+
importName: string,
11+
t: typeof Types,
12+
tagName: NodePath<Types.JSXIdentifier | Types.Identifier>,
13+
) {
14+
const sxIdentifier = addNamed(namePath, importName, process.env.PACKAGE_NAME as string);
15+
16+
const wrapWithSxCall = (expPath: NodePath<Types.Expression>) => {
17+
expPath.replaceWith(
18+
t.callExpression(sxIdentifier, [expPath.node, t.identifier(tagName.node.name)]),
19+
);
6820
};
69-
});
21+
22+
sxPropConvertor(expressionPath, wrapWithSxCall);
23+
// wrapWithSxCall(expressionPath);
24+
}
25+
26+
export const babelPlugin = declare<{ propName?: string; importName?: string }>(
27+
(api, { propName = 'sx', importName = 'sx' }) => {
28+
api.assertVersion(7);
29+
const { types: t } = api;
30+
return {
31+
name: '@pigmentcss/zero-babel-plugin',
32+
visitor: {
33+
JSXAttribute(path) {
34+
const namePath = path.get('name');
35+
const openingElement = path.findParent((p) => p.isJSXOpeningElement());
36+
if (
37+
!openingElement ||
38+
!openingElement.isJSXOpeningElement() ||
39+
!namePath.isJSXIdentifier() ||
40+
namePath.node.name !== propName
41+
) {
42+
return;
43+
}
44+
const tagName = openingElement.get('name');
45+
if (!tagName.isJSXIdentifier()) {
46+
return;
47+
}
48+
const valuePath = path.get('value');
49+
if (!valuePath.isJSXExpressionContainer()) {
50+
return;
51+
}
52+
const expressionPath = valuePath.get('expression');
53+
if (!expressionPath.isExpression()) {
54+
return;
55+
}
56+
replaceNodePath(expressionPath, namePath, importName, t, tagName);
57+
},
58+
ObjectProperty(path) {
59+
// @TODO - Maybe add support for React.createElement calls as well.
60+
// Right now, it only checks for jsx(),jsxs(),jsxDEV() and jsxsDEV() calls.
61+
const keyPath = path.get('key');
62+
if (!keyPath.isIdentifier() || keyPath.node.name !== propName) {
63+
return;
64+
}
65+
const valuePath = path.get('value');
66+
if (!valuePath.isObjectExpression() && !valuePath.isArrowFunctionExpression()) {
67+
return;
68+
}
69+
const parentJsxCall = path.findParent((p) => p.isCallExpression());
70+
if (!parentJsxCall || !parentJsxCall.isCallExpression()) {
71+
return;
72+
}
73+
const callee = parentJsxCall.get('callee');
74+
if (!callee.isIdentifier() || !callee.node.name.includes('jsx')) {
75+
return;
76+
}
77+
const jsxElement = parentJsxCall.get('arguments')[0] as NodePath<Types.Identifier>;
78+
replaceNodePath(valuePath, keyPath, importName, t, jsxElement);
79+
},
80+
},
81+
};
82+
},
83+
);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NodePath } from '@babel/core';
2+
import { ArrowFunctionExpression, Expression, ObjectExpression } from '@babel/types';
3+
import { sxObjectExtractor } from './sxObjectExtractor';
4+
5+
function isAllowedExpression(
6+
node: NodePath<Expression>,
7+
): node is NodePath<ObjectExpression> | NodePath<ArrowFunctionExpression> {
8+
return node.isObjectExpression() || node.isArrowFunctionExpression();
9+
}
10+
11+
export function sxPropConvertor(
12+
node: NodePath<Expression>,
13+
wrapWithSxCall: (expPath: NodePath<Expression>) => void,
14+
) {
15+
if (node.isConditionalExpression()) {
16+
const consequent = node.get('consequent');
17+
const alternate = node.get('alternate');
18+
19+
if (isAllowedExpression(consequent)) {
20+
sxObjectExtractor(consequent);
21+
wrapWithSxCall(consequent);
22+
}
23+
if (isAllowedExpression(alternate)) {
24+
sxObjectExtractor(alternate);
25+
wrapWithSxCall(alternate);
26+
}
27+
} else if (node.isLogicalExpression()) {
28+
const right = node.get('right');
29+
if (isAllowedExpression(right)) {
30+
sxObjectExtractor(right);
31+
wrapWithSxCall(right);
32+
}
33+
} else if (isAllowedExpression(node)) {
34+
sxObjectExtractor(node);
35+
wrapWithSxCall(node);
36+
} else if (node.isIdentifier()) {
37+
const rootScope = node.scope.getProgramParent();
38+
const binding = node.scope.getBinding(node.node.name);
39+
// Simplest case, ie, const styles = {static object}
40+
// and is used as <Component sx={styles} />
41+
if (binding?.scope === rootScope) {
42+
wrapWithSxCall(node);
43+
}
44+
}
45+
}

packages/pigment-react/tests/fixtures/styled.input.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const cls1 = css`
1919
font-size: ${({ theme }) => theme.size.font.h1};
2020
`;
2121

22-
const SliderRail = styled('span', {
22+
export const SliderRail = styled('span', {
2323
name: 'MuiSlider',
2424
slot: 'Rail',
2525
})`

packages/pigment-react/tests/fixtures/styled.output.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const Component = /*#__PURE__*/_styled("div")({
66
classes: ["c1y26wbb"]
77
});
88
const cls1 = "ct00dwm";
9-
const SliderRail = /*#__PURE__*/_styled2("span", {
9+
export const SliderRail = /*#__PURE__*/_styled2("span", {
1010
name: 'MuiSlider',
1111
slot: 'Rail'
1212
})({
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { SliderRail } from './styled.input';
2+
3+
function App() {
4+
return <SliderRail sx={{ color: 'red' }} />;
5+
}
6+
7+
function App2(props) {
8+
return (
9+
<SliderRail
10+
sx={
11+
props.variant === 'secondary'
12+
? { color: props.isRed ? 'red' : 'blue' }
13+
: { backgroundColor: 'blue', color: 'white' }
14+
}
15+
/>
16+
);
17+
}
18+
19+
function App3(props) {
20+
return (
21+
<SliderRail sx={props.variant === 'secondary' && { color: props.isRed ? 'red' : 'blue' }} />
22+
);
23+
}
24+
25+
const textAlign = 'center';
26+
const styles4 = {
27+
mb: 1,
28+
textAlign,
29+
};
30+
31+
function App4(props) {
32+
return <SliderRail sx={styles4} />;
33+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.soujkwr.s1ne1757{color:red;}
2+
.soujkwr.s1novky8{color:var(--s1novky8-0);}
3+
.soujkwr.s1dedx85{background-color:blue;color:white;}
4+
.soujkwr.s37rrrj{color:var(--s37rrrj-0);}
5+
.soujkwr.swjt3r4{margin-bottom:8px;text-align:center;}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { SliderRail } from './styled.input';
2+
function App() {
3+
return <SliderRail sx={"s1ne1757"} />;
4+
}
5+
function App2(props) {
6+
return <SliderRail sx={props.variant === 'secondary' ? {
7+
className: "s1novky8",
8+
vars: {
9+
"s1novky8-0": [props.isRed ? 'red' : 'blue', false]
10+
}
11+
} : "s1dedx85"} />;
12+
}
13+
function App3(props) {
14+
return <SliderRail sx={props.variant === 'secondary' && {
15+
className: "s37rrrj",
16+
vars: {
17+
"s37rrrj-0": [props.isRed ? 'red' : 'blue', false]
18+
}
19+
}} />;
20+
}
21+
function App4(props) {
22+
return <SliderRail sx={"swjt3r4"} />;
23+
}

0 commit comments

Comments
 (0)