Skip to content

Commit d31194c

Browse files
authored
[pigment-css][react] Implement sx transform for system components (#41861)
1 parent 48191a8 commit d31194c

15 files changed

+276
-120
lines changed

packages/pigment-css-react/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/processors/
22
/utils/
33
LICENSE
4+
/private-runtime/

packages/pigment-css-react/exports/sx-plugin.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ Object.defineProperty(exports, '__esModule', {
22
value: true,
33
});
44

5-
exports.default = require('../utils/pre-linaria-plugin').babelPlugin;
5+
exports.default = require('../utils/sx-plugin').babelPlugin;

packages/pigment-css-react/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@
147147
},
148148
"require": "./build/RtlProvider.js",
149149
"default": "./build/RtlProvider.js"
150+
},
151+
"./private-runtime": {
152+
"types": "./private-runtime/index.d.ts",
153+
"import": {
154+
"types": "./private-runtime/index.d.mts",
155+
"default": "./private-runtime/index.mjs"
156+
},
157+
"require": "./private-runtime/index.js",
158+
"default": "./private-runtime/index.js"
150159
}
151160
},
152161
"nx": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import clsx from 'clsx';
3+
4+
function useSx(sx, className, style) {
5+
const sxClass = typeof sx === 'string' ? sx : sx?.className;
6+
const sxVars = sx && typeof sx !== 'string' ? sx.vars : undefined;
7+
const varStyles = {};
8+
9+
if (sxVars) {
10+
Object.entries(sxVars).forEach(([cssVariable, [value, isUnitLess]]) => {
11+
if (typeof value === 'string' || isUnitLess) {
12+
varStyles[`--${cssVariable}`] = value;
13+
} else {
14+
varStyles[`--${cssVariable}`] = `${value}px`;
15+
}
16+
});
17+
}
18+
19+
return {
20+
className: clsx(sxClass, className),
21+
style: {
22+
...varStyles,
23+
...style,
24+
},
25+
};
26+
}
27+
28+
/* eslint-disable-next-line react/prop-types */
29+
export const ForwardSx = React.forwardRef(({ sx, sxComponent, className, style, ...rest }, ref) => {
30+
const Component = sxComponent;
31+
return <Component ref={ref} {...rest} {...useSx(sx, className, style)} />;
32+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ForwardSx';

packages/pigment-css-react/src/utils/pre-linaria-plugin.ts

-82
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { addNamed } from '@babel/helper-module-imports';
2+
import { declare } from '@babel/helper-plugin-utils';
3+
import { NodePath } from '@babel/core';
4+
import * as Types from '@babel/types';
5+
6+
import { sxPropConverter } from './sxPropConverter';
7+
8+
function convertJsxMemberExpressionToMemberExpression(
9+
t: typeof Types,
10+
nodePath: NodePath<Types.JSXMemberExpression>,
11+
): Types.MemberExpression {
12+
const object = nodePath.get('object');
13+
const property = nodePath.get('property');
14+
15+
if (object.isJSXMemberExpression()) {
16+
return t.memberExpression(
17+
convertJsxMemberExpressionToMemberExpression(t, object),
18+
t.identifier(property.node.name),
19+
);
20+
}
21+
return t.memberExpression(
22+
t.identifier((object.node as Types.JSXIdentifier).name),
23+
t.identifier(property.node.name),
24+
);
25+
}
26+
27+
function wrapWithSxComponent(
28+
t: typeof Types,
29+
tagNamePath: NodePath<Types.JSXIdentifier | Types.JSXMemberExpression | Types.JSXNamespacedName>,
30+
sxComponentName: string,
31+
) {
32+
const sxComponent = addNamed(
33+
tagNamePath,
34+
sxComponentName,
35+
`${process.env.PACKAGE_NAME}/private-runtime`,
36+
);
37+
const jsxElement = tagNamePath.findParent((p) => p.isJSXElement());
38+
if (!jsxElement?.isJSXElement()) {
39+
return;
40+
}
41+
const component = t.jsxIdentifier(sxComponent.name);
42+
43+
const newChildren = (jsxElement.get('children') ?? []).map((child) => child.node);
44+
let sxComponentValue: Types.Identifier | Types.MemberExpression | null = null;
45+
46+
if (tagNamePath.isJSXIdentifier()) {
47+
sxComponentValue = t.identifier(tagNamePath.node.name);
48+
} else if (tagNamePath.isJSXMemberExpression()) {
49+
sxComponentValue = convertJsxMemberExpressionToMemberExpression(t, tagNamePath);
50+
}
51+
52+
const newElement = t.jsxElement(
53+
t.jsxOpeningElement(
54+
component,
55+
[
56+
t.jsxAttribute(
57+
t.jsxIdentifier('sxComponent'),
58+
t.jsxExpressionContainer(sxComponentValue ?? t.nullLiteral()),
59+
),
60+
...jsxElement
61+
.get('openingElement')
62+
.get('attributes')
63+
.map((attr) => attr.node),
64+
],
65+
!newChildren.length,
66+
),
67+
newChildren.length ? t.jsxClosingElement(component) : null,
68+
newChildren,
69+
!newChildren.length,
70+
);
71+
jsxElement.replaceWith(newElement);
72+
}
73+
74+
function replaceNodePath(
75+
expressionPath: NodePath<Types.Expression>,
76+
namePath: NodePath<Types.JSXIdentifier | Types.Identifier>,
77+
importName: string,
78+
t: typeof Types,
79+
tagNamePath: NodePath<
80+
Types.JSXIdentifier | Types.Identifier | Types.JSXMemberExpression | Types.MemberExpression
81+
>,
82+
sxComponentName: string,
83+
) {
84+
const sxIdentifier = addNamed(namePath, importName, process.env.PACKAGE_NAME as string);
85+
let wasSxTransformed = false;
86+
87+
const wrapWithSxCall = (expPath: NodePath<Types.Expression>) => {
88+
let tagNameArg: Types.Identifier | Types.MemberExpression | null = null;
89+
if (tagNamePath.isJSXIdentifier()) {
90+
tagNameArg = t.identifier(tagNamePath.node.name);
91+
} else if (tagNamePath.isJSXMemberExpression()) {
92+
tagNameArg = convertJsxMemberExpressionToMemberExpression(t, tagNamePath);
93+
} else {
94+
tagNameArg = tagNamePath.node as Types.Identifier | Types.MemberExpression;
95+
}
96+
expPath.replaceWith(t.callExpression(sxIdentifier, [expPath.node, tagNameArg]));
97+
wasSxTransformed = true;
98+
};
99+
100+
sxPropConverter(expressionPath, wrapWithSxCall);
101+
102+
if (wasSxTransformed) {
103+
if (tagNamePath.isJSXIdentifier() || tagNamePath.isJSXMemberExpression()) {
104+
wrapWithSxComponent(t, tagNamePath, sxComponentName);
105+
}
106+
}
107+
}
108+
109+
export const babelPlugin = declare<{
110+
propName?: string;
111+
importName?: string;
112+
sxComponentName?: string;
113+
}>((api, { propName = 'sx', importName = 'sx', sxComponentName = 'ForwardSx' }) => {
114+
api.assertVersion(7);
115+
const { types: t } = api;
116+
return {
117+
name: '@pigmentcss/sx-plugin',
118+
visitor: {
119+
JSXAttribute(path) {
120+
const namePath = path.get('name');
121+
const openingElement = path.findParent((p) => p.isJSXOpeningElement());
122+
if (
123+
!openingElement ||
124+
!openingElement.isJSXOpeningElement() ||
125+
!namePath.isJSXIdentifier() ||
126+
namePath.node.name !== propName
127+
) {
128+
return;
129+
}
130+
const tagName = openingElement.get('name');
131+
const valuePath = path.get('value');
132+
if (!valuePath.isJSXExpressionContainer()) {
133+
return;
134+
}
135+
const expressionPath = valuePath.get('expression');
136+
if (!expressionPath.isExpression()) {
137+
return;
138+
}
139+
// @ts-ignore
140+
replaceNodePath(expressionPath, namePath, importName, t, tagName, sxComponentName);
141+
},
142+
ObjectProperty(path) {
143+
// @TODO - Maybe add support for React.createElement calls as well.
144+
// Right now, it only checks for jsx(),jsxs(),jsxDEV() and jsxsDEV() calls.
145+
const keyPath = path.get('key');
146+
if (!keyPath.isIdentifier() || keyPath.node.name !== propName) {
147+
return;
148+
}
149+
const valuePath = path.get('value');
150+
if (!valuePath.isObjectExpression() && !valuePath.isArrowFunctionExpression()) {
151+
return;
152+
}
153+
const parentJsxCall = path.findParent((p) => p.isCallExpression());
154+
if (!parentJsxCall || !parentJsxCall.isCallExpression()) {
155+
return;
156+
}
157+
const callee = parentJsxCall.get('callee');
158+
if (!callee.isIdentifier() || !callee.node.name.includes('jsx')) {
159+
return;
160+
}
161+
const jsxElement = parentJsxCall.get('arguments')[0] as NodePath<Types.Identifier>;
162+
replaceNodePath(valuePath, keyPath, importName, t, jsxElement, sxComponentName);
163+
},
164+
},
165+
};
166+
});

packages/pigment-css-react/tests/Box/fixtures/box.output.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.bc1d15y {
1+
._1yemdgw {
22
margin: 0;
33
margin-block: 1rem;
44
padding: 0;
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { sx as _sx2 } from '@pigment-css/react';
2+
import { ForwardSx as _ForwardSx } from '@pigment-css/react/private-runtime';
13
import Box from '@pigment-css/react/Box';
24
export function App() {
35
return (
4-
<Box as="ul" sx={'bc1d15y'}>
6+
<_ForwardSx sxComponent={Box} as="ul" sx={'_1yemdgw'}>
57
Hello Box
6-
</Box>
8+
</_ForwardSx>
79
);
810
}

packages/pigment-css-react/tests/sx/fixtures/sxProps.output.css

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@
99
.sjfloo5-1 {
1010
font-size: 3rem;
1111
}
12-
.sjfloo5.s1o8xp19 {
12+
.sjfloo5._1o8xp19 {
1313
color: red;
1414
}
15-
.sjfloo5.s1xbsywq {
16-
color: var(--s1xbsywq-0);
15+
.sjfloo5._1xbsywq {
16+
color: var(--_1xbsywq-0);
1717
}
18-
.sjfloo5.s1wnk6s5 {
18+
.sjfloo5._1wnk6s5 {
1919
background-color: blue;
2020
color: white;
2121
}
22-
.sjfloo5.stzaibv {
23-
color: var(--stzaibv-0);
22+
.sjfloo5._tzaibv {
23+
color: var(--_tzaibv-0);
2424
}
25-
.sjfloo5.sazg8ol {
25+
.sjfloo5._azg8ol {
2626
margin-bottom: 8px;
2727
text-align: center;
2828
}

0 commit comments

Comments
 (0)