Repository: azat-io/eslint-plugin-perfectionist (ts)

Issue:
Feature: sort-named-exports needs ignoreAlias
### What rule do you want to change?

sort-named-exports

### Describe the problem

[sort-named-exports](https://perfectionist.dev/rules/sort-named-exports) needs an `ignoreAlias` option (as [sort-named-imports](https://perfectionist.dev/rules/sort-named-imports#ignorealias) has)

### Code example

This export statement
```typescript
export {
  throttle as a,
  calculateAge as b,
  formatDate as c,
  generateUUID as d,
  debounce as e,
  parseQueryString as f
} from "./utils";
```
will always be sorted as it is ☝️ (by aliases). But the perfectionist inside me says that there should be an option to sort it as:
```typescript
export {
  calculateAge as b,
  debounce as e,
  formatDate as c,
  generateUUID as d,
  parseQueryString as f,
  throttle as a
} from "./utils";
```

### Additional comments

Thank you for this awesome stuff!

### Validations

- [x] Read the [docs](https://perfectionist.dev).
- [x] Check that there isn't [already an issue](https://github.com/azat-io/eslint-plugin-perfectionist/issues) that reports the same bug to avoid creating a duplicate.

Pull request description:
feat(sort-named-exports): adds `ignoreAlias` option
Resolves #454

# Description

This PR adds the `ignoreAlias` option (similar to `sort-named-imports`) to `sort-named-exports`.

# ℹ️ Note

According to the AST, the following syntax is allowed

```ts
export { 'literal' as local } from './import'
```

I don't think this is valid in any Typescript version, but I've added a handler for that case anyway since it costs nothing 🤷
229fe1f581e28c60f85bf42708e8d4c844355ba2

### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->

- [x] New Feature

Patch:
diff --git a/docs/content/rules/sort-named-exports.mdx b/docs/content/rules/sort-named-exports.mdx
index 2c85c47..9fefe30 100644
--- a/docs/content/rules/sort-named-exports.mdx
+++ b/docs/content/rules/sort-named-exports.mdx
@@ -155,6 +155,15 @@ Specifies the sorting locales. See [String.prototype.localeCompare() - locales](
 - `string` — A BCP 47 language tag (e.g. `'en'`, `'en-US'`, `'zh-CN'`).
 - `string[]` — An array of BCP 47 language tags.
 
+### ignoreAlias
+
+<sub>default: `false`</sub>
+
+Determines whether to use the export alias as the name for sorting instead of the local name.
+
+- `true` — Use the export alias for sorting.
+- `false` — Use the local name for sorting.
+
 ### groupKind
 
 <sub>default: `'mixed'`</sub>
@@ -225,9 +234,12 @@ Each group of members (separated by empty lines) is treated independently, and t
                 {
                   type: 'alphabetical',
                   order: 'asc',
+                  ignoreAlias: false,
                   ignoreCase: true,
                   specialCharacters: 'keep',
                   groupKind: 'mixed',
+                  partitionByNewLine: false,
+                  partitionByComment: false,
                 },
               ],
             },
@@ -250,9 +262,12 @@ Each group of members (separated by empty lines) is treated independently, and t
               {
                 type: 'alphabetical',
                 order: 'asc',
+                ignoreAlias: false,
                 ignoreCase: true,
                 specialCharacters: 'keep',
                 groupKind: 'mixed',
+                partitionByNewLine: false,
+                partitionByComment: false,
               },
             ],
           },
diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts
index 57a945e..540af0c 100644
--- a/rules/sort-named-exports.ts
+++ b/rules/sort-named-exports.ts
@@ -33,6 +33,7 @@ type Options = [
       groupKind: 'values-first' | 'types-first' | 'mixed'
       partitionByComment: PartitionByCommentOption
       partitionByNewLine: boolean
+      ignoreAlias: boolean
       type: TypeOption
     } & CommonOptions
   >,
@@ -50,6 +51,7 @@ let defaultOptions: Required<Options[0]> = {
   partitionByNewLine: false,
   partitionByComment: false,
   type: 'alphabetical',
+  ignoreAlias: false,
   groupKind: 'mixed',
   ignoreCase: true,
   locales: 'en-US',
@@ -76,15 +78,23 @@ export default createEslintRule<Options, MESSAGE_ID>({
 
       let formattedMembers: SortNamedExportsSortingNode[][] = [[]]
       for (let specifier of node.specifiers) {
-        let groupKind: 'value' | 'type' =
-          specifier.exportKind === 'type' ? 'type' : ('value' as const)
-
         let name: string
 
-        if (specifier.exported.type === 'Identifier') {
-          ;({ name } = specifier.exported)
+        if (options.ignoreAlias) {
+          if (specifier.local.type === 'Identifier') {
+            ;({ name } = specifier.local)
+          } else {
+            // Should not be allowed in typescript, but is possible according to
+            // The AST
+            // Ex: `export { 'literal' as local } from './import'`
+            name = specifier.local.value
+          }
         } else {
-          name = specifier.exported.value
+          if (specifier.exported.type === 'Identifier') {
+            ;({ name } = specifier.exported)
+          } else {
+            name = specifier.exported.value
+          }
         }
 
         let lastSortingNode = formattedMembers.at(-1)?.at(-1)
@@ -93,9 +103,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
             specifier,
             eslintDisabledLines,
           ),
+          groupKind: specifier.exportKind === 'type' ? 'type' : 'value',
           size: rangeToDiff(specifier, sourceCode),
           node: specifier,
-          groupKind,
           name,
         }
 
@@ -161,6 +171,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
             description: 'Specifies top-level groups.',
             type: 'string',
           },
+          ignoreAlias: {
+            description: 'Controls whether to ignore alias names.',
+            type: 'boolean',
+          },
           partitionByComment: partitionByCommentJsonSchema,
           partitionByNewLine: partitionByNewLineJsonSchema,
           type: buildTypeJsonSchema(),


Test patch:
diff --git a/test/rules/sort-named-exports.test.ts b/test/rules/sort-named-exports.test.ts
index 2d0fcb3..0ce70f2 100644
--- a/test/rules/sort-named-exports.test.ts
+++ b/test/rules/sort-named-exports.test.ts
@@ -778,6 +778,70 @@ describe(ruleName, () => {
         valid: [],
       },
     )
+
+    ruleTester.run(`${ruleName}(${type}): handles "ignoreAlias" option`, rule, {
+      invalid: [
+        {
+          errors: [
+            {
+              data: {
+                right: 'a',
+                left: 'b',
+              },
+              messageId: 'unexpectedNamedExportsOrder',
+            },
+          ],
+          options: [
+            {
+              ...options,
+              ignoreAlias: true,
+            },
+          ],
+          output: dedent`
+            export {
+              a as b,
+              b as a,
+            }
+          `,
+          code: dedent`
+            export {
+              b as a,
+              a as b,
+            }
+          `,
+        },
+        {
+          errors: [
+            {
+              data: {
+                right: 'a',
+                left: 'b',
+              },
+              messageId: 'unexpectedNamedExportsOrder',
+            },
+          ],
+          output: dedent`
+            export {
+              'a' as b,
+              'b' as a,
+            } from './module'
+          `,
+          code: dedent`
+            export {
+              'b' as a,
+              'a' as b,
+            } from './module'
+          `,
+          options: [
+            {
+              ...options,
+              ignoreAlias: true,
+            },
+          ],
+        },
+      ],
+      valid: [],
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {