-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c0ed6dc
commit dc003c0
Showing
13 changed files
with
938 additions
and
1 deletion.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import type { SortingNode } from './types' | ||
|
||
interface BaseCompareOptions { | ||
/** | ||
* Custom function to get the value of the node. By default, returns the node's name. | ||
*/ | ||
nodeValueGetter?: (node: SortingNode) => string | ||
order: 'desc' | 'asc' | ||
} | ||
|
||
interface AlphabeticalCompareOptions extends BaseCompareOptions { | ||
type: 'alphabetical' | ||
ignoreCase?: boolean | ||
} | ||
|
||
interface LineLengthCompareOptions extends BaseCompareOptions { | ||
maxLineLength?: number | ||
type: 'line-length' | ||
} | ||
|
||
interface NaturalCompareOptions extends BaseCompareOptions { | ||
ignoreCase?: boolean | ||
type: 'natural' | ||
} | ||
|
||
export type CompareOptions = | ||
| AlphabeticalCompareOptions | ||
// | LineLengthCompareOptions | ||
| NaturalCompareOptions | ||
|
||
export function compare(a: SortingNode, b: SortingNode, options: CompareOptions): number { | ||
/** Don't sort unsassigned imports. */ | ||
if (a.group === 'unassigned' || b.group === 'unassigned') return 0 | ||
|
||
if (b.dependencies?.includes(a.name)) { | ||
return -1 | ||
} | ||
|
||
if (a.dependencies?.includes(b.name)) { | ||
return 1 | ||
} | ||
|
||
let orderCoefficient = options.order === 'asc' ? 1 : -1 | ||
let sortingFunction: (a: SortingNode, b: SortingNode) => number | ||
|
||
let formatString = | ||
options.type === 'line-length' || !options.ignoreCase | ||
? (string: string) => string | ||
: (string: string) => string.toLowerCase() | ||
|
||
let nodeValueGetter = options.nodeValueGetter ?? ((node: SortingNode) => node.name) | ||
|
||
if (options.type === 'alphabetical') { | ||
sortingFunction = (aNode, bNode) => | ||
formatString(nodeValueGetter(aNode)).localeCompare(formatString(nodeValueGetter(bNode))) | ||
} else if (options.type === 'natural') { | ||
let prepareNumeric = (string: string) => { | ||
let formattedNumberPattern = /^[+-]?[\d ,_]+(\.[\d ,_]+)?$/ | ||
if (formattedNumberPattern.test(string)) { | ||
return string.replaceAll(/[ ,_]/g, '') | ||
} | ||
|
||
return string | ||
} | ||
|
||
sortingFunction = (aNode, bNode) => { | ||
let aImport = stripProtocol(nodeValueGetter(aNode)) | ||
let bImport = stripProtocol(nodeValueGetter(bNode)) | ||
|
||
if (aImport.startsWith('.') && bImport.startsWith('.')) { | ||
return compareDotSegments(aImport, bImport) | ||
} | ||
|
||
return compareString(aImport, bImport) | ||
} | ||
// | ||
// naturalCompare( | ||
// prepareNumeric(formatString(nodeValueGetter(aNode))), | ||
// prepareNumeric(formatString(nodeValueGetter(bNode))), | ||
// ) | ||
} else { | ||
sortingFunction = (aNode, bNode) => { | ||
let aSize = aNode.size | ||
let bSize = bNode.size | ||
|
||
let { maxLineLength } = options | ||
|
||
if (maxLineLength) { | ||
let isTooLong = (size: number, node: SortingNode) => | ||
size > maxLineLength && node.hasMultipleImportDeclarations | ||
|
||
if (isTooLong(aSize, aNode)) { | ||
aSize = nodeValueGetter(aNode).length + 10 | ||
} | ||
|
||
if (isTooLong(bSize, bNode)) { | ||
bSize = nodeValueGetter(bNode).length + 10 | ||
} | ||
} | ||
|
||
return aSize - bSize | ||
} | ||
} | ||
|
||
return orderCoefficient * sortingFunction(a, b) | ||
} | ||
|
||
function stripProtocol(name: string) { | ||
return name.replace(/^(node|bun):/, '') | ||
} | ||
|
||
function compareString(first: string, second: string) { | ||
return first.localeCompare(second, 'en', { numeric: true }) | ||
} | ||
|
||
function compareDotSegments(first: string, second: string) { | ||
let regex = /\.+(?=\/)/g | ||
|
||
let firstCount = (first.match(regex) ?? []).join('').length | ||
let secondCount = (second.match(regex) ?? []).join('').length | ||
|
||
if (secondCount < firstCount) return -1 | ||
if (firstCount < secondCount) return 1 | ||
|
||
// If segment length is the same, compare the path alphabetically. | ||
return compareString(first, second) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { AST_TOKEN_TYPES, type TSESLint, type TSESTree } from '@typescript-eslint/utils' | ||
|
||
export function getCommentBefore( | ||
node: TSESTree.Node, | ||
source: TSESLint.SourceCode, | ||
): TSESTree.Comment | undefined { | ||
let [tokenBefore, tokenOrCommentBefore] = source.getTokensBefore(node, { | ||
filter: ({ value, type }) => | ||
!(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)), | ||
includeComments: true, | ||
count: 2, | ||
}) as Array<TSESTree.Token | undefined> | ||
|
||
if ( | ||
(tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Block || | ||
tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Line) && | ||
node.loc.start.line - tokenOrCommentBefore.loc.end.line <= 1 && | ||
tokenBefore?.loc.end.line !== tokenOrCommentBefore.loc.start.line | ||
) { | ||
return tokenOrCommentBefore | ||
} | ||
|
||
return undefined | ||
} | ||
|
||
export function getCommentAfter( | ||
node: TSESTree.Node, | ||
source: TSESLint.SourceCode, | ||
): TSESTree.Comment | undefined { | ||
let token = source.getTokenAfter(node, { | ||
filter: ({ value, type }) => | ||
!(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)), | ||
includeComments: true, | ||
}) | ||
|
||
if ( | ||
(token?.type === AST_TOKEN_TYPES.Block || token?.type === AST_TOKEN_TYPES.Line) && | ||
node.loc.end.line === token.loc.end.line | ||
) { | ||
return token | ||
} | ||
|
||
return undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Group, SortingNode } from './types.js' | ||
|
||
export function getGroupNumber(groups: Group[], node: SortingNode): number { | ||
for (let max = groups.length, index = 0; index < max; index++) { | ||
let currentGroup = groups[index] | ||
|
||
if ( | ||
node.group === currentGroup || | ||
(Array.isArray(currentGroup) && | ||
typeof node.group === 'string' && | ||
currentGroup.includes(node.group)) | ||
) { | ||
return index | ||
} | ||
} | ||
|
||
return groups.length | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { TSESLint } from '@typescript-eslint/utils' | ||
|
||
import type { SortingNode } from './types.js' | ||
|
||
export function getLinesBetween( | ||
source: TSESLint.SourceCode, | ||
left: SortingNode, | ||
right: SortingNode, | ||
) { | ||
let linesBetween = source.lines.slice(left.node.loc.end.line, right.node.loc.start.line - 1) | ||
|
||
return linesBetween.filter((line) => line.trim().length === 0).length | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ASTUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils' | ||
|
||
import { getCommentBefore } from './get-comment.js' | ||
import { isPartitionComment } from './is-partition-comment.js' | ||
|
||
export function getNodeRange( | ||
node: TSESTree.Node, | ||
sourceCode: TSESLint.SourceCode, | ||
additionalOptions?: { | ||
partitionComment?: string[] | boolean | string | ||
}, | ||
): TSESTree.Range { | ||
let start = node.range.at(0)! | ||
let end = node.range.at(1)! | ||
|
||
let raw = sourceCode.text.slice(start, end) | ||
|
||
if (ASTUtils.isParenthesized(node, sourceCode)) { | ||
let bodyOpeningParen = sourceCode.getTokenBefore(node, ASTUtils.isOpeningParenToken)! | ||
|
||
let bodyClosingParen = sourceCode.getTokenAfter(node, ASTUtils.isClosingParenToken)! | ||
|
||
start = bodyOpeningParen.range.at(0)! | ||
end = bodyClosingParen.range.at(1)! | ||
} | ||
|
||
let comment = getCommentBefore(node, sourceCode) | ||
|
||
if (raw.endsWith(';') || raw.endsWith(',')) { | ||
let tokensAfter = sourceCode.getTokensAfter(node, { | ||
includeComments: true, | ||
count: 2, | ||
}) | ||
|
||
if (node.loc.start.line === tokensAfter.at(1)?.loc.start.line) { | ||
end -= 1 | ||
} | ||
} | ||
|
||
if ( | ||
comment && | ||
!isPartitionComment(additionalOptions?.partitionComment ?? false, comment.value) | ||
) { | ||
start = comment.range.at(0)! | ||
} | ||
|
||
return [start, end] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { minimatch } from 'minimatch' | ||
|
||
export const isPartitionComment = ( | ||
partitionComment: string[] | boolean | string, | ||
comment: string, | ||
) => | ||
(Array.isArray(partitionComment) && | ||
partitionComment.some((pattern) => | ||
minimatch(comment.trim(), pattern, { | ||
nocomment: true, | ||
}), | ||
)) ?? | ||
(typeof partitionComment === 'string' && | ||
minimatch(comment.trim(), partitionComment, { | ||
nocomment: true, | ||
})) ?? | ||
partitionComment === true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export function pairwise<T>(nodes: T[], callback: (left: T, right: T, iteration: number) => void) { | ||
if (nodes.length > 1) { | ||
for (let index = 1; index < nodes.length; index++) { | ||
let left = nodes.at(index - 1) | ||
let right = nodes.at(index) | ||
|
||
if (left && right) { | ||
callback(left, right, index - 1) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { compare, type CompareOptions } from './compare.js' | ||
import type { SortingNode } from './types' | ||
|
||
export function sortNodes<T extends SortingNode>(nodes: T[], options: CompareOptions): T[] { | ||
return [...nodes].sort((a, b) => compare(a, b, options)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils' | ||
|
||
export interface SortingNode<Node extends TSESTree.Node = TSESTree.Node> { | ||
name: string | ||
node: Node | ||
dependencies?: string[] | ||
group?: string | ||
hasMultipleImportDeclarations?: boolean | ||
} | ||
|
||
export type Group = | ||
| 'unassigned' | ||
| 'builtin' | ||
| 'framework' | ||
| 'external' | ||
| 'internal' | ||
| 'local' | ||
| 'style' | ||
| 'object' | ||
| 'unknown' | ||
|
||
export interface Options { | ||
groups: Group[] | ||
ignoreCase: boolean | ||
newlinesBetween: 'ignore' | 'always' | 'never' | ||
order: 'asc' | 'desc' | ||
type: 'alphabetical' | 'natural' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Group } from './types' | ||
|
||
export function useGroups(groups: string[]) { | ||
let group: undefined | string | ||
// For lookup performance | ||
let groupsSet = new Set(groups.flat()) | ||
|
||
let defineGroup = (value: Group, override = false) => { | ||
if ((!group || override) && groupsSet.has(value)) { | ||
group = value | ||
} | ||
} | ||
|
||
return { | ||
getGroup: () => group ?? 'unknown', | ||
defineGroup, | ||
} | ||
} |
Oops, something went wrong.