From 8a9a07d1235fa41e763b144ace0f56aec20fe91e Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Wed, 19 Nov 2025 18:31:32 +0100 Subject: [PATCH 1/3] Add sortFirst option to jsx-sort-props rule --- docs/rules/jsx-sort-props.md | 31 ++++++- lib/rules/jsx-sort-props.js | 67 +++++++++++++++ tests/lib/rules/jsx-sort-props.js | 132 +++++++++++++++++++++++++++++- 3 files changed, 227 insertions(+), 3 deletions(-) diff --git a/docs/rules/jsx-sort-props.md b/docs/rules/jsx-sort-props.md index b51209f32e..d1b93009c7 100644 --- a/docs/rules/jsx-sort-props.md +++ b/docs/rules/jsx-sort-props.md @@ -13,7 +13,7 @@ This rule checks all JSX components and verifies that all props are sorted alpha Examples of **incorrect** code for this rule: ```jsx -; + ``` Examples of **correct** code for this rule: @@ -35,6 +35,7 @@ Examples of **correct** code for this rule: "ignoreCase": , "noSortAlphabetically": , "reservedFirst": |>, + "sortFirst": >, "locale": "auto" | "any valid locale" }] ... @@ -47,7 +48,7 @@ When `true` the rule ignores the case-sensitivity of the props order. Examples of **correct** code for this rule ```jsx -; + ``` ### `callbacksLast` @@ -138,6 +139,32 @@ With `reservedFirst: ["key"]`, the following will **not** warn: ``` +### `sortFirst` + +When `sortFirst` is defined as an array of prop names, those props must be listed before all other props, maintaining the exact order specified in the array. This option has the highest priority and takes precedence over all other sorting options (including `reservedFirst`, `shorthandFirst`, `callbacksLast`, and `multiline`). + +The prop names in the array are matched case-sensitively by default, but respect the `ignoreCase` option when enabled. + +Examples of **incorrect** code for this rule: + +```jsx +// 'jsx-sort-props': [1, { sortFirst: ['className'] }] + +``` + +Examples of **correct** code for this rule: + +```jsx +// 'jsx-sort-props': [1, { sortFirst: ['className'] }] + + +// 'jsx-sort-props': [1, { sortFirst: ['className', 'id'] }] + + +// 'jsx-sort-props': [1, { sortFirst: ['className'], ignoreCase: true }] + +``` + ### `locale` Defaults to `"auto"`, meaning, the locale of the current environment. diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index ec9d869b74..4a62ad9e6e 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -35,6 +35,7 @@ const messages = { listShorthandLast: 'Shorthand props must be listed after all other props', listMultilineFirst: 'Multiline props must be listed before all other props', listMultilineLast: 'Multiline props must be listed after all other props', + listSortFirstPropsFirst: 'Props in sortFirst must be listed before all other props', sortPropsByAlpha: 'Props should be sorted alphabetically', }; @@ -49,6 +50,20 @@ function isReservedPropName(name, list) { return list.indexOf(name) >= 0; } +function getSortFirstIndex(name, sortFirstList, ignoreCase) { + if (!sortFirstList || sortFirstList.length === 0) { + return -1; + } + const normalizedPropName = ignoreCase ? name.toLowerCase() : name; + for (let i = 0; i < sortFirstList.length; i++) { + const normalizedListName = ignoreCase ? sortFirstList[i].toLowerCase() : sortFirstList[i]; + if (normalizedPropName === normalizedListName) { + return i; + } + } + return -1; +} + let attributeMap; // attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end } @@ -70,6 +85,24 @@ function contextCompare(a, b, options) { return -1; } + if (options.sortFirst && options.sortFirst.length > 0) { + const aSortFirstIndex = getSortFirstIndex(aProp, options.sortFirst, options.ignoreCase); + const bSortFirstIndex = getSortFirstIndex(bProp, options.sortFirst, options.ignoreCase); + if (aSortFirstIndex >= 0 && bSortFirstIndex >= 0) { + // Both are in sortFirst, maintain their exact order + if (aSortFirstIndex !== bSortFirstIndex) { + return aSortFirstIndex - bSortFirstIndex; + } + return 0; + } + if (aSortFirstIndex >= 0 && bSortFirstIndex < 0) { + return -1; + } + if (aSortFirstIndex < 0 && bSortFirstIndex >= 0) { + return 1; + } + } + if (options.reservedFirst) { const aIsReserved = isReservedPropName(aProp, options.reservedList); const bIsReserved = isReservedPropName(bProp, options.reservedList); @@ -222,6 +255,7 @@ function generateFixerFunction(node, context, reservedList) { const multiline = configuration.multiline || 'ignore'; const noSortAlphabetically = configuration.noSortAlphabetically || false; const reservedFirst = configuration.reservedFirst || false; + const sortFirst = configuration.sortFirst || []; const locale = configuration.locale || 'auto'; // Sort props according to the context. Only supports ignoreCase. @@ -236,6 +270,7 @@ function generateFixerFunction(node, context, reservedList) { noSortAlphabetically, reservedFirst, reservedList, + sortFirst, locale, }; const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context); @@ -382,6 +417,12 @@ module.exports = { reservedFirst: { type: ['array', 'boolean'], }, + sortFirst: { + type: 'array', + items: { + type: 'string', + }, + }, locale: { type: 'string', default: 'auto', @@ -402,6 +443,7 @@ module.exports = { const reservedFirst = configuration.reservedFirst || false; const reservedFirstError = validateReservedFirstConfig(context, reservedFirst); const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST; + const sortFirst = configuration.sortFirst || []; const locale = configuration.locale || 'auto'; return { @@ -425,6 +467,31 @@ module.exports = { const previousIsCallback = propTypesSortUtil.isCallbackPropName(previousPropName); const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName); + if (sortFirst && sortFirst.length > 0) { + const previousSortFirstIndex = getSortFirstIndex(previousPropName, sortFirst, ignoreCase); + const currentSortFirstIndex = getSortFirstIndex(currentPropName, sortFirst, ignoreCase); + + if (previousSortFirstIndex >= 0 && currentSortFirstIndex >= 0) { + // Both are in sortFirst, check their order + if (previousSortFirstIndex > currentSortFirstIndex) { + reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList); + return memo; + } + return decl; + } + + if (previousSortFirstIndex >= 0 && currentSortFirstIndex < 0) { + // Previous is in sortFirst, current is not - this is correct, continue to next prop + return decl; + } + + if (previousSortFirstIndex < 0 && currentSortFirstIndex >= 0) { + // Current is in sortFirst but previous is not - error + reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList); + return memo; + } + } + if (ignoreCase) { previousPropName = previousPropName.toLowerCase(); currentPropName = currentPropName.toLowerCase(); diff --git a/tests/lib/rules/jsx-sort-props.js b/tests/lib/rules/jsx-sort-props.js index 797dc26650..38d598a10f 100644 --- a/tests/lib/rules/jsx-sort-props.js +++ b/tests/lib/rules/jsx-sort-props.js @@ -58,6 +58,10 @@ const expectedReservedFirstError = { messageId: 'listReservedPropsFirst', type: 'JSXIdentifier', }; +const expectedSortFirstError = { + messageId: 'listSortFirstPropsFirst', + type: 'JSXIdentifier', +}; const expectedEmptyReservedFirstError = { messageId: 'listIsEmpty', }; @@ -120,6 +124,33 @@ const multilineAndShorthandAndCallbackLastArgs = [ callbacksLast: true, }, ]; +const sortFirstArgs = [{ sortFirst: ['className'] }]; +const sortFirstMultipleArgs = [{ sortFirst: ['className', 'id'] }]; +const sortFirstWithIgnoreCaseArgs = [{ sortFirst: ['className'], ignoreCase: true }]; +const sortFirstWithReservedFirstArgs = [ + { + sortFirst: ['className'], + reservedFirst: true, + }, +]; +const sortFirstWithShorthandFirstArgs = [ + { + sortFirst: ['className'], + shorthandFirst: true, + }, +]; +const sortFirstWithCallbacksLastArgs = [ + { + sortFirst: ['className'], + callbacksLast: true, + }, +]; +const sortFirstWithMultilineFirstArgs = [ + { + sortFirst: ['className'], + multiline: 'first', + }, +]; ruleTester.run('jsx-sort-props', rule, { valid: parsers.all([].concat( @@ -296,7 +327,29 @@ ruleTester.run('jsx-sort-props', rule, { /> `, options: [{ locale: 'sk-SK' }], - } : [] + } : [], + // sortFirst + { code: ';', options: sortFirstArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstWithReservedFirstArgs }, + { code: ';', options: sortFirstWithShorthandFirstArgs }, + { code: ';', options: sortFirstWithCallbacksLastArgs }, + { + code: ` + + `, + options: sortFirstWithMultilineFirstArgs, + }, + { code: ';', options: sortFirstWithIgnoreCaseArgs } )), invalid: parsers.all([].concat( { @@ -1101,6 +1154,83 @@ ruleTester.run('jsx-sort-props', rule, { line: 11, }, ], + }, + // sortFirst + { + code: ';', + options: sortFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithReservedFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithShorthandFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithCallbacksLastArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ` + + `, + options: sortFirstWithMultilineFirstArgs, + errors: [expectedSortFirstError, expectedMultilineFirstError], + output: ` + + `, + }, + { + code: ';', + options: sortFirstWithIgnoreCaseArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedSortFirstError], + output: ';', } )), }); From d58017a174e3de9e2b712862bb75baadfca983b3 Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Wed, 19 Nov 2025 18:55:25 +0100 Subject: [PATCH 2/3] remove useless check --- lib/rules/jsx-sort-props.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index 4a62ad9e6e..4ca0906fc5 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -51,9 +51,6 @@ function isReservedPropName(name, list) { } function getSortFirstIndex(name, sortFirstList, ignoreCase) { - if (!sortFirstList || sortFirstList.length === 0) { - return -1; - } const normalizedPropName = ignoreCase ? name.toLowerCase() : name; for (let i = 0; i < sortFirstList.length; i++) { const normalizedListName = ignoreCase ? sortFirstList[i].toLowerCase() : sortFirstList[i]; From cc1c24cfe307a210f1a4a18f3aa4b24147d1f51a Mon Sep 17 00:00:00 2001 From: Charles Francoise Date: Thu, 20 Nov 2025 10:23:35 +0100 Subject: [PATCH 3/3] limit ESLint version for exported type tests --- test-published-types/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-published-types/package.json b/test-published-types/package.json index 80953c53ab..412a588ea4 100644 --- a/test-published-types/package.json +++ b/test-published-types/package.json @@ -3,6 +3,6 @@ "private": true, "version": "0.0.0", "dependencies": { - "eslint": "^9.11.1" + "eslint": "~9.38.0" } }