Skip to content

Commit

Permalink
feat: add class order sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbbreuer committed Apr 23, 2023
1 parent 3f8cea6 commit b4d496f
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"antfu",
"applypatch",
"astro",
"attributify",
"automagically",
"booleanish",
"breakline",
Expand All @@ -73,6 +74,7 @@
"changelist",
"changelogithub",
"circleci",
"classname",
"classpath",
"clippy",
"cmake",
Expand Down Expand Up @@ -112,6 +114,7 @@
"intlify",
"jiti",
"jsdelivr",
"Laravel",
"lighthouserc",
"linebreak",
"lintstagedrc",
Expand Down Expand Up @@ -148,11 +151,13 @@
"stroustrup",
"styleci",
"stylelint",
"synckit",
"tazerc",
"terserrc",
"textlint",
"treeshaking",
"tsdoc",
"TSES",
"tsup",
"typecheck",
"unocss",
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ Forked from [`@ow3/eslint-config`](https://github.com/antfu/eslint-config)

###### Changes in this fork

- Type-Safe Error Handling
- Type-Safe error handling
- Improved component library linting & formatting
- Stacks Support
- Laravel Support
- Stacks support
- Laravel support
- UnoCSS class ordering support
- Other minor additions, i.e. `no-constant-binary-expression` usage

## Usage
Expand Down
1 change: 0 additions & 1 deletion packages/eslint-config/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module.exports = {
extends: [
'@ow3/eslint-config-vue',
'@unocss',
],
}
1 change: 0 additions & 1 deletion packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"@ow3/eslint-config-vue": "workspace:*",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@unocss/eslint-config": "^0.51.6",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.5",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-ow3/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
'src/dirs',
'src/index',
'src/worker-sort',
],
declaration: true,
clean: true,
Expand Down
5 changes: 4 additions & 1 deletion packages/eslint-plugin-ow3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@typescript-eslint/utils": "^5.59.0"
"@typescript-eslint/utils": "^5.59.0",
"@unocss/config": "0.51.6",
"@unocss/core": "0.51.6",
"synckit": "^0.8.5"
},
"devDependencies": {
"@types/node": "^18.15.11",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-ow3/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CLASS_FIELDS = ['class', 'classname']
export const AST_NODES_WITH_QUOTES = ['Literal', 'VLiteral']
3 changes: 3 additions & 0 deletions packages/eslint-plugin-ow3/src/dirs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { fileURLToPath } from 'node:url'

export const distDir = fileURLToPath(new URL('../dist', import.meta.url))
2 changes: 2 additions & 0 deletions packages/eslint-plugin-ow3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ifNewline from './rules/if-newline'
import importDedupe from './rules/import-dedupe'
import preferInlineTypeImport from './rules/prefer-inline-type-import'
import topLevelFunction from './rules/top-level-function'
import orderClasses from './rules/order-classes'

export default {
rules: {
Expand All @@ -11,5 +12,6 @@ export default {
'prefer-inline-type-import': preferInlineTypeImport,
'generic-spacing': genericSpacing,
'top-level-function': topLevelFunction,
'order-classes': orderClasses,
},
}
82 changes: 82 additions & 0 deletions packages/eslint-plugin-ow3/src/rules/order-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// thank you to @unocss/eslint-plugin
// https://github.com/unocss/unocss/blob/main/packages/eslint-plugin/src/rules/order.ts
import { join } from 'node:path'
import { ESLintUtils } from '@typescript-eslint/utils'
import { createSyncFn } from 'synckit'
import type { RuleListener } from '@typescript-eslint/utils/dist/ts-eslint'
import type { TSESTree } from '@typescript-eslint/types'
import { distDir } from '../dirs'
import { AST_NODES_WITH_QUOTES, CLASS_FIELDS } from '../constants'

const sortClasses = createSyncFn<(classes: string) => Promise<string>>(join(distDir, 'worker-sort.cjs'))

export default ESLintUtils.RuleCreator(name => name)({
name: 'order',
meta: {
type: 'layout',
fixable: 'code',
docs: {
description: 'Order of utilities in class attribute',
recommended: 'warn',
},
messages: {
'invalid-order': 'Utility classes are not ordered',
},
schema: [],
},
defaultOptions: [],
create(context) {
function checkLiteral(node: TSESTree.Literal) {
if (typeof node.value !== 'string' || !node.value.trim())
return
const input = node.value
const sorted = sortClasses(input).trim()
if (sorted !== input) {
context.report({
node,
messageId: 'invalid-order',
fix(fixer) {
if (AST_NODES_WITH_QUOTES.includes(node.type))
return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], sorted)
else
return fixer.replaceText(node, sorted)
},
})
}
}

const scriptVisitor: RuleListener = {
JSXAttribute(node) {
if (typeof node.name.name === 'string' && CLASS_FIELDS.includes(node.name.name.toLowerCase()) && node.value) {
if (node.value.type === 'Literal')
checkLiteral(node.value)
}
},
SvelteAttribute(node: any) {
if (node.key.name === 'class') {
if (node.value?.[0].type === 'SvelteLiteral')
checkLiteral(node.value[0])
}
},
}

const templateBodyVisitor: RuleListener = {
VAttribute(node: any) {
if (node.key.name === 'class') {
if (node.value.type === 'VLiteral')
checkLiteral(node.value)
}
},
}

// @ts-expect-error missing-types
if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) {
return scriptVisitor
}
else {
// For Vue
// @ts-expect-error missing-types
return context.parserServices?.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
}
},
})
45 changes: 45 additions & 0 deletions packages/eslint-plugin-ow3/src/sort-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// thank you to https://github.com/unocss/unocss/blob/main/packages/shared-integration/src/sort-rules.ts
import type { UnoGenerator } from '@unocss/core'
import { collapseVariantGroup, notNull, parseVariantGroup } from '@unocss/core'

export async function sortRules(rules: string, uno: UnoGenerator) {
const unknown: string[] = []

// enable details for variant handlers
if (!uno.config.details)
uno.config.details = true

// const hasAttributify = !!uno.config.presets.find(i => i.name === '@unocss/preset-attributify')
// const hasVariantGroup = !!uno.config.transformers?.find(i => i.name === '@unocss/transformer-variant-group')

const expandedResult = parseVariantGroup(rules) // todo read separators from config
rules = expandedResult.expanded

const result = await Promise.all(rules.split(/\s+/g)
.map(async (i) => {
const token = await uno.parseToken(i)
if (token == null) {
unknown.push(i)
return undefined
}
const variantRank = (token[0][5]?.variantHandlers?.length || 0) * 100_000
const order = token[0][0] + variantRank
return [order, i] as const
}))

let sorted = result
.filter(notNull)
.sort((a, b) => {
let result = a[0] - b[0]
if (result === 0)
result = a[1].localeCompare(b[1])
return result
})
.map(i => i[1])
.join(' ')

if (expandedResult?.prefixes.length)
sorted = collapseVariantGroup(sorted, expandedResult.prefixes)

return [...unknown, sorted].join(' ').trim()
}
20 changes: 20 additions & 0 deletions packages/eslint-plugin-ow3/src/worker-sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { loadConfig } from '@unocss/config'
import type { UnoGenerator } from '@unocss/core'
import { createGenerator } from '@unocss/core'
import { runAsWorker } from 'synckit'
import { sortRules } from './sort-rules'

async function getGenerator() {
const { config, sources } = await loadConfig()
if (!sources.length)
throw new Error('[@ow3/eslint-plugin] No `uno.config.ts` file found. Please create a bug report.')
return createGenerator(config)
}

let promise: Promise<UnoGenerator<any>> | undefined

runAsWorker(async (classes: string) => {
promise = promise || getGenerator()
const uno = await promise
return await sortRules(classes, uno)
})
47 changes: 14 additions & 33 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b4d496f

Please sign in to comment.