From 481c0b2853bcd7e35bfa50e65db00ab884108dec Mon Sep 17 00:00:00 2001 From: Anay Bhakat Date: Thu, 19 Mar 2026 14:14:58 -0700 Subject: [PATCH] [eslint-plugin] Add flex shorthand expansion to stylex-valid-shorthands Add flex shorthand expansion following the createSpecificTransformer pattern. The rule now reports an error and autofixes when users write `flex: '1 0 auto'` instead of the expanded longhands. Handles all CSS spec cases: - Keywords: auto (1 1 auto), none (0 0 auto), initial (0 1 auto) - Single number: flex-grow with shrink=1, basis=0% - Single basis: grow=1, shrink=1 with the basis value - Two values: grow+shrink or grow+basis - Three values: grow shrink basis - calc(), var(), min-content, max-content, fit-content as basis - !important support --- .../__tests__/stylex-valid-shorthands-test.js | 298 ++++++++++++++++++ .../src/stylex-valid-shorthands.js | 1 + .../src/utils/splitShorthands.js | 130 ++++++++ 3 files changed, 429 insertions(+) diff --git a/packages/@stylexjs/eslint-plugin/__tests__/stylex-valid-shorthands-test.js b/packages/@stylexjs/eslint-plugin/__tests__/stylex-valid-shorthands-test.js index fffd44135..b300cce89 100644 --- a/packages/@stylexjs/eslint-plugin/__tests__/stylex-valid-shorthands-test.js +++ b/packages/@stylexjs/eslint-plugin/__tests__/stylex-valid-shorthands-test.js @@ -1752,5 +1752,303 @@ eslintTester.run('stylex-valid-shorthands', rule.default, { }, ], }, + // flex: single number expands to grow/shrink/basis + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: 1, + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '1', + flexBasis: '0%', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 1" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: string single number + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '2', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '2', + flexShrink: '1', + flexBasis: '0%', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 2" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: auto keyword + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: 'auto', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '1', + flexBasis: 'auto', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: auto" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: none keyword + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: 'none', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '0', + flexShrink: '0', + flexBasis: 'auto', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: none" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: initial keyword + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: 'initial', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '0', + flexShrink: '1', + flexBasis: 'auto', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: initial" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: single basis value (with unit) + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '100px', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '1', + flexBasis: '100px', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 100px" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: two numbers (grow shrink) + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '1 0', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '0', + flexBasis: '0%', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 1 0" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: number + basis + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '1 30px', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '1', + flexBasis: '30px', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 1 30px" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: three values (grow shrink basis) + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '2 2 10%', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '2', + flexShrink: '2', + flexBasis: '10%', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 2 2 10%" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: with !important (allowImportant) + { + options: [{ allowImportant: true }], + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '1 0 auto !important', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1 !important', + flexShrink: '0 !important', + flexBasis: 'auto !important', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 1 0 auto !important" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, + // flex: calc() basis + { + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flex: '1 1 calc(100% - 20px)', + }, + }); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + flexGrow: '1', + flexShrink: '1', + flexBasis: 'calc(100% - 20px)', + }, + }); + `, + errors: [ + { + message: + 'Property shorthands using multiple values like "flex: 1 1 calc(100% - 20px)" are not supported in StyleX. Separate into individual properties.', + }, + ], + }, ], }); diff --git a/packages/@stylexjs/eslint-plugin/src/stylex-valid-shorthands.js b/packages/@stylexjs/eslint-plugin/src/stylex-valid-shorthands.js index 390608188..8f9995ab4 100644 --- a/packages/@stylexjs/eslint-plugin/src/stylex-valid-shorthands.js +++ b/packages/@stylexjs/eslint-plugin/src/stylex-valid-shorthands.js @@ -58,6 +58,7 @@ const shorthandAliases: $ReadOnly<{ gridRow: createSpecificTransformer('grid-row'), gridTemplate: createSpecificTransformer('grid-template'), outline: createSpecificTransformer('outline'), + flex: createSpecificTransformer('flex'), margin: createDirectionalTransformer('margin', 'Block', 'Inline'), padding: createDirectionalTransformer('padding', 'Block', 'Inline'), marginBlock: createBlockInlineTransformer('margin', 'Block'), diff --git a/packages/@stylexjs/eslint-plugin/src/utils/splitShorthands.js b/packages/@stylexjs/eslint-plugin/src/utils/splitShorthands.js index 21a2ab024..3c8f22a3a 100644 --- a/packages/@stylexjs/eslint-plugin/src/utils/splitShorthands.js +++ b/packages/@stylexjs/eslint-plugin/src/utils/splitShorthands.js @@ -546,6 +546,126 @@ function parseBorderParts(values: Array): ?{ return { width, style, color }; } +const FLEX_BASIS_KEYWORDS = new Set([ + 'auto', + 'content', + 'min-content', + 'max-content', + 'fit-content', +]); +const FLEX_BASIS_FUNCTION_REGEX = /^(?:calc|min|max|clamp|fit-content)\(/i; +const FLEX_NUMBER_REGEX = /^-?(?:\d+|\d*\.\d+)$/; +const FLEX_UNITLESS_ZERO_REGEX = /^-?(?:0|0\.0+)$/; + +function isFlexNumberValue(value: string): boolean { + return FLEX_NUMBER_REGEX.test(value); +} + +function isFlexBasisValue( + value: string, + options: { allowUnitlessZero?: boolean } = {}, +): boolean { + const lower = value.toLowerCase(); + if (options.allowUnitlessZero && FLEX_UNITLESS_ZERO_REGEX.test(lower)) { + return true; + } + return ( + FLEX_BASIS_KEYWORDS.has(lower) || + FLEX_BASIS_FUNCTION_REGEX.test(lower) || + lower.startsWith('var(') || + /^-?(?:\d+|\d*\.\d+)(?:[a-z%]+)$/i.test(lower) + ); +} + +function expandFlexShorthand( + values: Array, + importantSuffix: string, +): ?$ReadOnlyArray<$ReadOnly<[string, string]>> { + if (values.length === 1) { + const val = values[0]; + const lower = val.toLowerCase(); + if (lower === 'auto') { + return [ + ['flexGrow', applyImportant('1', importantSuffix)], + ['flexShrink', applyImportant('1', importantSuffix)], + ['flexBasis', applyImportant('auto', importantSuffix)], + ]; + } + if (lower === 'none') { + return [ + ['flexGrow', applyImportant('0', importantSuffix)], + ['flexShrink', applyImportant('0', importantSuffix)], + ['flexBasis', applyImportant('auto', importantSuffix)], + ]; + } + if (lower === 'initial') { + return [ + ['flexGrow', applyImportant('0', importantSuffix)], + ['flexShrink', applyImportant('1', importantSuffix)], + ['flexBasis', applyImportant('auto', importantSuffix)], + ]; + } + if (isFlexNumberValue(val)) { + // Single unitless number = flex-grow + return [ + ['flexGrow', applyImportant(val, importantSuffix)], + ['flexShrink', applyImportant('1', importantSuffix)], + ['flexBasis', applyImportant('0%', importantSuffix)], + ]; + } + if (isFlexBasisValue(val)) { + return [ + ['flexGrow', applyImportant('1', importantSuffix)], + ['flexShrink', applyImportant('1', importantSuffix)], + ['flexBasis', applyImportant(val, importantSuffix)], + ]; + } + return null; + } + + if (values.length === 2) { + const [first, second] = values; + if (!isFlexNumberValue(first)) { + return null; + } + if (isFlexNumberValue(second)) { + // + return [ + ['flexGrow', applyImportant(first, importantSuffix)], + ['flexShrink', applyImportant(second, importantSuffix)], + ['flexBasis', applyImportant('0%', importantSuffix)], + ]; + } + if (isFlexBasisValue(second)) { + // + return [ + ['flexGrow', applyImportant(first, importantSuffix)], + ['flexShrink', applyImportant('1', importantSuffix)], + ['flexBasis', applyImportant(second, importantSuffix)], + ]; + } + return null; + } + + if (values.length === 3) { + const [grow, shrink, basis] = values; + if ( + !isFlexNumberValue(grow) || + !isFlexNumberValue(shrink) || + !isFlexBasisValue(basis, { allowUnitlessZero: true }) + ) { + return null; + } + return [ + ['flexGrow', applyImportant(grow, importantSuffix)], + ['flexShrink', applyImportant(shrink, importantSuffix)], + ['flexBasis', applyImportant(basis, importantSuffix)], + ]; + } + + return null; +} + function expandBorderSideShorthand( property: string, values: Array, @@ -807,6 +927,16 @@ export function splitSpecificShorthands( return expanded.length > 0 ? expanded : [['gridArea', CANNOT_FIX]]; } + if (property === 'flex') { + const flexSplit = splitTopLevelValueTokens(baseValue); + if (flexSplit.hasTopLevelComma || flexSplit.hasTopLevelSlash) { + return [['flex', CANNOT_FIX]]; + } + const flexValues = flexSplit.parts.map((part) => part.text); + const expandedFlex = expandFlexShorthand(flexValues, importantSuffix); + return expandedFlex ?? [['flex', CANNOT_FIX]]; + } + const splitValues = splitTopLevelValueTokens(baseValue); if (splitValues.parts.length <= 1 && !splitValues.hasTopLevelSlash) { return [[toCamelCase(property), isNumber ? Number(rawValue) : rawValue]];