Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
[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.
  • Loading branch information
brijeshb42 committed Mar 8, 2024
commit 8f3c3987db9ac034b34e1de9c0031cb229eeba16
18 changes: 9 additions & 9 deletions packages/pigment-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@
"stylis": "^4.3.1"
},
"devDependencies": {
"@babel/plugin-syntax-jsx": "^7.23.3",
"@types/babel__core": "^7.20.5",
"@types/babel__helper-module-imports": "^7.18.3",
"@types/babel__helper-plugin-utils": "^7.10.3",
"@types/chai": "^4.3.12",
"@types/cssesc": "^3.0.2",
"@types/lodash": "^4.14.202",
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.21",
"@types/react": "^18.2.55",
"@types/stylis": "^4.2.5",
Expand Down Expand Up @@ -133,15 +135,6 @@
}
},
"nx": {
"targetDefaults": {
"build": {
"outputs": [
"{projectRoot}/build",
"{projectRoot}/processors",
"{projectRoot}/utils"
]
}
},
"targets": {
"test": {
"cache": false,
Expand All @@ -154,6 +147,13 @@
"dependsOn": [
"build"
]
},
"build": {
"outputs": [
"{projectRoot}/build",
"{projectRoot}/processors",
"{projectRoot}/utils"
]
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/pigment-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as keyframes } from './keyframes';
export { generateAtomics, atomics } from './generateAtomics';
export { default as css } from './css';
export { default as createUseThemeProps } from './createUseThemeProps';
export { clsx } from 'clsx';
143 changes: 78 additions & 65 deletions packages/pigment-react/src/utils/pre-linaria-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,82 @@
import { addNamed } from '@babel/helper-module-imports';
import { declare } from '@babel/helper-plugin-utils';
import { sxObjectExtractor } from './sxObjectExtractor';
import { NodePath } from '@babel/core';
import * as Types from '@babel/types';
import { sxPropConvertor } from './sxPropConverter';

export const babelPlugin = declare((api) => {
api.assertVersion(7);
const { types: t } = api;
return {
name: '@pigmentcss/zero-babel-plugin',
visitor: {
JSXAttribute(path) {
const namePath = path.get('name');
const openingElement = path.findParent((p) => p.isJSXOpeningElement());
if (
!openingElement ||
!openingElement.isJSXOpeningElement() ||
!namePath.isJSXIdentifier() ||
namePath.node.name !== 'sx'
) {
return;
}
const tagName = openingElement.get('name');
if (!tagName.isJSXIdentifier()) {
return;
}
const valuePath = path.get('value');
if (!valuePath.isJSXExpressionContainer()) {
return;
}
const expressionPath = valuePath.get('expression');
if (!expressionPath.isExpression()) {
return;
}
if (!expressionPath.isObjectExpression() && !expressionPath.isArrowFunctionExpression()) {
return;
}
sxObjectExtractor(expressionPath);
const sxIdentifier = addNamed(namePath, 'sx', process.env.PACKAGE_NAME as string);
expressionPath.replaceWith(
t.callExpression(sxIdentifier, [expressionPath.node, t.identifier(tagName.node.name)]),
);
},
ObjectProperty(path) {
// @TODO - Maybe add support for React.createElement calls as well.
// Right now, it only checks for jsx(),jsxs(),jsxDEV() and jsxsDEV() calls.
const keyPath = path.get('key');
if (!keyPath.isIdentifier() || keyPath.node.name !== 'sx') {
return;
}
const valuePath = path.get('value');
if (!valuePath.isObjectExpression() && !valuePath.isArrowFunctionExpression()) {
return;
}
const parentJsxCall = path.findParent((p) => p.isCallExpression());
if (!parentJsxCall || !parentJsxCall.isCallExpression()) {
return;
}
const callee = parentJsxCall.get('callee');
if (!callee.isIdentifier() || !callee.node.name.includes('jsx')) {
return;
}
const jsxElement = parentJsxCall.get('arguments')[0];
sxObjectExtractor(valuePath);
const sxIdentifier = addNamed(keyPath, 'sx', process.env.PACKAGE_NAME as string);
valuePath.replaceWith(t.callExpression(sxIdentifier, [valuePath.node, jsxElement.node]));
},
},
function replaceNodePath(
expressionPath: NodePath<Types.Expression>,
namePath: NodePath<Types.JSXIdentifier | Types.Identifier>,
importName: string,
t: typeof Types,
tagName: NodePath<Types.JSXIdentifier | Types.Identifier>,
) {
const sxIdentifier = addNamed(namePath, importName, process.env.PACKAGE_NAME as string);

const wrapWithSxCall = (expPath: NodePath<Types.Expression>) => {
expPath.replaceWith(
t.callExpression(sxIdentifier, [expPath.node, t.identifier(tagName.node.name)]),
);
};
});

sxPropConvertor(expressionPath, wrapWithSxCall);
}

export const babelPlugin = declare<{ propName?: string; importName?: string }>(
(api, { propName = 'sx', importName = 'sx' }) => {
api.assertVersion(7);
const { types: t } = api;
return {
name: '@pigmentcss/zero-babel-plugin',
visitor: {
JSXAttribute(path) {
const namePath = path.get('name');
const openingElement = path.findParent((p) => p.isJSXOpeningElement());
if (
!openingElement ||
!openingElement.isJSXOpeningElement() ||
!namePath.isJSXIdentifier() ||
namePath.node.name !== propName
) {
return;
}
const tagName = openingElement.get('name');
if (!tagName.isJSXIdentifier()) {
return;
}
const valuePath = path.get('value');
if (!valuePath.isJSXExpressionContainer()) {
return;
}
const expressionPath = valuePath.get('expression');
if (!expressionPath.isExpression()) {
return;
}
replaceNodePath(expressionPath, namePath, importName, t, tagName);
},
ObjectProperty(path) {
// @TODO - Maybe add support for React.createElement calls as well.
// Right now, it only checks for jsx(),jsxs(),jsxDEV() and jsxsDEV() calls.
const keyPath = path.get('key');
if (!keyPath.isIdentifier() || keyPath.node.name !== propName) {
return;
}
const valuePath = path.get('value');
if (!valuePath.isObjectExpression() && !valuePath.isArrowFunctionExpression()) {
return;
}
const parentJsxCall = path.findParent((p) => p.isCallExpression());
if (!parentJsxCall || !parentJsxCall.isCallExpression()) {
return;
}
const callee = parentJsxCall.get('callee');
if (!callee.isIdentifier() || !callee.node.name.includes('jsx')) {
return;
}
const jsxElement = parentJsxCall.get('arguments')[0] as NodePath<Types.Identifier>;
replaceNodePath(valuePath, keyPath, importName, t, jsxElement);
},
},
};
},
);
45 changes: 45 additions & 0 deletions packages/pigment-react/src/utils/sxPropConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NodePath } from '@babel/core';
import { ArrowFunctionExpression, Expression, ObjectExpression } from '@babel/types';
import { sxObjectExtractor } from './sxObjectExtractor';

function isAllowedExpression(
node: NodePath<Expression>,
): node is NodePath<ObjectExpression> | NodePath<ArrowFunctionExpression> {
return node.isObjectExpression() || node.isArrowFunctionExpression();
}

export function sxPropConvertor(
node: NodePath<Expression>,
wrapWithSxCall: (expPath: NodePath<Expression>) => void,
) {
if (node.isConditionalExpression()) {
const consequent = node.get('consequent');
const alternate = node.get('alternate');

if (isAllowedExpression(consequent)) {
sxObjectExtractor(consequent);
wrapWithSxCall(consequent);
}
if (isAllowedExpression(alternate)) {
sxObjectExtractor(alternate);
wrapWithSxCall(alternate);
}
} else if (node.isLogicalExpression()) {
const right = node.get('right');
if (isAllowedExpression(right)) {
sxObjectExtractor(right);
wrapWithSxCall(right);
}
} else if (isAllowedExpression(node)) {
sxObjectExtractor(node);
wrapWithSxCall(node);
} else if (node.isIdentifier()) {
const rootScope = node.scope.getProgramParent();
const binding = node.scope.getBinding(node.node.name);
// Simplest case, ie, const styles = {static object}
// and is used as <Component sx={styles} />
if (binding?.scope === rootScope) {
wrapWithSxCall(node);
}
}
}
94 changes: 94 additions & 0 deletions packages/pigment-react/tests/pigment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { expect } from 'chai';
import { asyncResolveFallback } from '@wyw-in-js/shared';
import { transformAsync } from '@babel/core';
import { TransformCacheCollection, transform, createFileReporter } from '@wyw-in-js/transform';
import { preprocessor } from '@pigment-css/react/utils';
import sxTransformPlugin from '../exports/sx-plugin';

const files = fs
.readdirSync(path.join(__dirname, 'fixtures'))
.filter((file) => file.endsWith('.input.js'));

const theme = {
palette: {
primary: {
main: 'red',
},
},
size: {
font: {
h1: '3rem',
},
},
components: {
MuiSlider: {
styleOverrides: {
rail: {
fontSize: '3rem',
},
},
},
},
};

async function transformWithSx(code: string, filename: string) {
const babelResult = await transformAsync(code, {
babelrc: false,
configFile: false,
filename,
plugins: ['@babel/plugin-syntax-jsx', [sxTransformPlugin]],
});
const pluginOptions = {
themeArgs: {
theme,
},
babelOptions: {
configFile: false,
babelrc: false,
plugins: ['@babel/plugin-syntax-jsx'],
},
tagResolver(source: string, tag: string) {
if (source !== '@pigment-css/react') {
return null;
}
return require.resolve(`../exports/${tag}`);
},
};
const cache = new TransformCacheCollection();
const { emitter: eventEmitter } = createFileReporter(false);
return transform(
{
options: {
filename,
preprocessor,
pluginOptions,
},
cache,
eventEmitter,
},
babelResult?.code ?? code,
asyncResolveFallback,
);
}

describe('zero-runtime', () => {
files.forEach((file) => {
it(`test input file ${file}`, async () => {
const inputFilePath = path.join(__dirname, 'fixtures', file);
const outputFilePath = path.join(__dirname, 'fixtures', file.replace('.input.', '.output.'));
const outputCssFilePath = path.join(
__dirname,
'fixtures',
file.replace('.input.js', '.output.css'),
);
const inputContent = fs.readFileSync(inputFilePath, 'utf8');
const outputContent = fs.readFileSync(outputFilePath, 'utf8');
const outputCssContent = fs.readFileSync(outputCssFilePath, 'utf8');
const result = await transformWithSx(inputContent, inputFilePath);
expect(result.cssText).to.equal(outputCssContent);
expect(result.code.trim()).to.equal(outputContent.trim());
});
});
});
4 changes: 2 additions & 2 deletions packages/pigment-react/tests/styled/fixtures/styled.input.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ const Component = styled.div(({ theme }) => ({
animation: `${rotateKeyframe} 2s ease-out 0s infinite`,
}));

const SliderRail = styled('span', {
export const SliderRail = styled('span', {
name: 'MuiSlider',
slot: 'Rail',
})`
display: none;
display: block;
position: absolute;
border-radius: inherit;
background-color: currentColor;
Expand Down
33 changes: 33 additions & 0 deletions packages/pigment-react/tests/styled/fixtures/sxProps.input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { SliderRail } from './styled.input';

function App() {
return <SliderRail sx={{ color: 'red' }} />;
}

function App2(props) {
return (
<SliderRail
sx={
props.variant === 'secondary'
? { color: props.isRed ? 'red' : 'blue' }
: { backgroundColor: 'blue', color: 'white' }
}
/>
);
}

function App3(props) {
return (
<SliderRail sx={props.variant === 'secondary' && { color: props.isRed ? 'red' : 'blue' }} />
);
}

const textAlign = 'center';
const styles4 = {
mb: 1,
textAlign,
};

function App4(props) {
return <SliderRail sx={styles4} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.soujkwr.s1ne1757{color:red;}
.soujkwr.s1novky8{color:var(--s1novky8-0);}
.soujkwr.s1dedx85{background-color:blue;color:white;}
.soujkwr.s37rrrj{color:var(--s37rrrj-0);}
.soujkwr.swjt3r4{margin-bottom:8px;text-align:center;}
Loading