diff --git a/packages/@stylexjs/eslint-plugin/README.md b/packages/@stylexjs/eslint-plugin/README.md index 9dba3b75f..7ae43ea42 100644 --- a/packages/@stylexjs/eslint-plugin/README.md +++ b/packages/@stylexjs/eslint-plugin/README.md @@ -256,7 +256,8 @@ specifications. ### `@stylexjs/no-conflicting-props` This rule disallows using `className` or `style` props on elements that spread -`stylex.props()` to avoid conflicts and unexpected behavior. +`stylex.props()` or use StyleX JSX shorthand to avoid conflicts and unexpected +behavior. #### Invalid examples @@ -264,12 +265,20 @@ This rule disallows using `className` or `style` props on elements that spread
+ +
+ +
``` #### Config options ```json { - "validImports": ["stylex", "@stylexjs/stylex"] + "validImports": ["stylex", "@stylexjs/stylex"], + "sxPropName": "sx" } ``` + +Set `sxPropName` to a string to check a custom JSX shorthand prop, or `false` to +disable JSX shorthand checks. diff --git a/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-conflicting-props-test.js b/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-conflicting-props-test.js index abb36e3b9..010b45593 100644 --- a/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-conflicting-props-test.js +++ b/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-conflicting-props-test.js @@ -52,6 +52,22 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, { } `, }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + function Component() { + return
; + } + `, + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + function Component() { + return
; + } + `, + }, { code: ` import * as stylex from '@stylexjs/stylex'; @@ -109,6 +125,40 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, { } `, }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return ; + } + `, + }, + { + options: [{ sxPropName: false }], + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + }, ], invalid: [ { @@ -304,5 +354,91 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, { }, ], }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + errors: [ + { + message: + 'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.', + }, + ], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + errors: [ + { + message: + 'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.', + }, + ], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + errors: [ + { + message: + 'The `style` prop should not be used with the `sx` StyleX prop to avoid conflicts.', + }, + ], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + errors: [ + { + message: + 'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.', + }, + ], + }, + { + options: [{ sxPropName: 'css' }], + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { color: 'red' }, + }); + function Component() { + return
; + } + `, + errors: [ + { + message: + 'The `className` prop should not be used with the `css` StyleX prop to avoid conflicts.', + }, + ], + }, ], }); diff --git a/packages/@stylexjs/eslint-plugin/src/stylex-no-conflicting-props.js b/packages/@stylexjs/eslint-plugin/src/stylex-no-conflicting-props.js index e8da8f805..21ff8ec76 100644 --- a/packages/@stylexjs/eslint-plugin/src/stylex-no-conflicting-props.js +++ b/packages/@stylexjs/eslint-plugin/src/stylex-no-conflicting-props.js @@ -18,9 +18,15 @@ type JSXIdentifier = { +name: string, }; +type JSXExpressionContainer = { + +type: 'JSXExpressionContainer', + +expression: Node | { +type: 'JSXEmptyExpression' }, +}; + type JSXAttribute = { +type: 'JSXAttribute', +name: JSXIdentifier | { +type: string }, + +value?: Node | JSXExpressionContainer | null, }; type JSXSpreadAttribute = { @@ -30,15 +36,22 @@ type JSXSpreadAttribute = { type JSXOpeningElement = { +type: 'JSXOpeningElement', + +name: JSXIdentifier | { +type: string }, +attributes: $ReadOnlyArray, }; +const STYLEX_PROPS_CONFLICTING_PROPS_MESSAGE = + 'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.'; + +const STYLEX_SHORTHAND_CONFLICTING_PROPS_MESSAGE = + 'The `{{propName}}` prop should not be used with the `{{sxPropName}}` StyleX prop to avoid conflicts.'; + const stylexNoConflictingProps = { meta: { type: 'problem', docs: { description: - 'Disallow using `className` or `style` props on elements that spread `stylex.props()`', + 'Disallow using `className` or `style` props on elements that spread `stylex.props()` or use StyleX JSX shorthand', category: 'Best Practices', recommended: true, }, @@ -62,14 +75,20 @@ const stylexNoConflictingProps = { }, default: ['stylex', '@stylexjs/stylex'], }, + sxPropName: { + oneOf: [{ type: 'string' }, { enum: [false] }], + default: 'sx', + }, }, additionalProperties: false, }, ], }, create(context: Rule.RuleContext): { ... } { - const { validImports: importsToLookFor = ['stylex', '@stylexjs/stylex'] } = - context.options[0] || {}; + const { + validImports: importsToLookFor = ['stylex', '@stylexjs/stylex'], + sxPropName = 'sx', + } = context.options[0] || {}; const importTracker = createImportTracker(importsToLookFor); @@ -85,6 +104,30 @@ const stylexNoConflictingProps = { ); } + function isLowercaseHostElement(node: JSXOpeningElement): boolean { + return ( + node.name.type === 'JSXIdentifier' && + typeof node.name.name === 'string' && + node.name.name[0] === node.name.name[0].toLowerCase() + ); + } + + function hasStylexShorthandProp(node: JSXOpeningElement): boolean { + return ( + typeof sxPropName === 'string' && + isLowercaseHostElement(node) && + node.attributes.some( + (attr) => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === sxPropName && + attr.value != null && + attr.value.type === 'JSXExpressionContainer' && + attr.value.expression.type !== 'JSXEmptyExpression', + ) + ); + } + return { ImportDeclaration: importTracker.ImportDeclaration, @@ -96,10 +139,16 @@ const stylexNoConflictingProps = { isStylexPropsCallee(attr.argument.callee), ); - if (!hasStylexPropsSpread) { + const hasStylexShorthand = hasStylexShorthandProp(node); + + if (!hasStylexPropsSpread && !hasStylexShorthand) { return; } + const message = hasStylexShorthand + ? STYLEX_SHORTHAND_CONFLICTING_PROPS_MESSAGE + : STYLEX_PROPS_CONFLICTING_PROPS_MESSAGE; + for (const attr of node.attributes) { if ( attr.type === 'JSXAttribute' && @@ -109,9 +158,8 @@ const stylexNoConflictingProps = { context.report({ // $FlowFixMe[incompatible-type] node: attr, - message: - 'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.', - data: { propName: attr.name.name }, + message, + data: { propName: attr.name.name, sxPropName }, }); } else if ( attr.type === 'JSXSpreadAttribute' && @@ -127,9 +175,8 @@ const stylexNoConflictingProps = { context.report({ // $FlowFixMe[incompatible-type] node: prop, - message: - 'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.', - data: { propName: prop.key.name }, + message, + data: { propName: prop.key.name, sxPropName }, }); } }