Skip to content

Commit

Permalink
feat(esling): new @unocss/blocklist rule, #3082
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Sep 4, 2023
1 parent 3392020 commit 0956f35
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 37 deletions.
18 changes: 18 additions & 0 deletions docs/integrations/eslint.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ In `.eslintrc`:

- `@unocss/order` - Enforce a specific order for class selectors.
- `@unocss/order-attributify` - Enforce a specific order for attributify selectors.
- `@unocss/blocklist` - Disallow specific class selectors [Optional].

### `@unocss/blocklist`

Throw warning or error when using utilities listed in `blocklist` get matched.

This rule is not enabled by default. To enable it, add the following to your `.eslintrc`:

```jsonc
{
"extends": [
"@unocss"
],
"rules": {
"@unocss/blocklist": "warn" // or "error"
}
}
```

## Prior Arts

Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default defineBuildConfig({
entries: [
'src/dirs',
'src/index',
'src/worker-sort',
'src/worker',
],
clean: true,
declaration: false,
Expand Down
6 changes: 5 additions & 1 deletion packages/eslint-plugin/fixtures/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"root": true,
"extends": [
"@antfu",
"plugin:@unocss/recommended"
]
],
"rules": {
"@unocss/blocklist": "error"
}
}
7 changes: 4 additions & 3 deletions packages/eslint-plugin/fixtures/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
<div class="hover:(text-red text-4xl) hover:mx1 mx1 m2">Variant group</div>

<div class="
foo bar
hover:(text-red text-4xl)
hover:mx1 mx1 m2
foo bar
hover:(text-red text-4xl)
hover:mx1 mx1 m2
blocked-rule
"></div>
</div>
</template>
3 changes: 3 additions & 0 deletions packages/eslint-plugin/fixtures/uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ export default defineConfig({
transformers: [
transformerVariantGroup(),
],
blocklist: [
(i) => i.includes('blocked')
]
})
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import order from './rules/order'
import orderAttributify from './rules/order-attributify'
import blocklist from './rules/blocklist'
import configsRecommended from './configs/recommended'

export default {
rules: {
order,
'order-attributify': orderAttributify,
blocklist,
},
configs: {
recommended: configsRecommended,
Expand Down
6 changes: 6 additions & 0 deletions packages/eslint-plugin/src/rules/_.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { join } from 'node:path'
import { createSyncFn } from 'synckit'
import { distDir } from '../dirs'
import type { run } from '../worker'

export const syncAction = createSyncFn(join(distDir, 'worker.cjs')) as typeof run
73 changes: 73 additions & 0 deletions packages/eslint-plugin/src/rules/blocklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ESLintUtils } from '@typescript-eslint/utils'
import type { RuleListener } from '@typescript-eslint/utils/ts-eslint'
import type { TSESTree } from '@typescript-eslint/types'
import { CLASS_FIELDS } from '../constants'
import { syncAction } from './_'

export default ESLintUtils.RuleCreator(name => name)({
name: 'blocklist',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'Utilities in UnoCSS blocklist',
recommended: 'recommended',
},
messages: {
'in-blocklist': 'Utility \'{{ name }}\' is in blocklist',
},
schema: [],
},
defaultOptions: [],
create(context) {
const checkLiteral = (node: TSESTree.Literal) => {
if (typeof node.value !== 'string' || !node.value.trim())
return
const input = node.value
const blocked = syncAction('blocklist', input, context.filename)
blocked.forEach((i) => {
context.report({
node,
messageId: 'in-blocklist',
data: {
name: i,
},
})
})
}

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)
}
},
})
8 changes: 2 additions & 6 deletions packages/eslint-plugin/src/rules/order-attributify.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { join } from 'node:path'
import { ESLintUtils } from '@typescript-eslint/utils'
import { createSyncFn } from 'synckit'
import type { RuleListener } from '@typescript-eslint/utils/ts-eslint'
import type { TSESTree } from '@typescript-eslint/types'
import MagicString from 'magic-string'
import { distDir } from '../dirs'

const sortClasses = createSyncFn<(classes: string) => Promise<string>>(join(distDir, 'worker-sort.cjs'))
import { syncAction } from './_'

const IGNORE_ATTRIBUTES = ['style', 'class', 'classname', 'value']

Expand Down Expand Up @@ -36,7 +32,7 @@ export default ESLintUtils.RuleCreator(name => name)({
return

const input = valueless.map((i: any) => i.key.name).join(' ').trim()
const sorted = sortClasses(input)
const sorted = syncAction('sort', input)
if (sorted !== input) {
context.report({
node,
Expand Down
8 changes: 2 additions & 6 deletions packages/eslint-plugin/src/rules/order.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { join } from 'node:path'
import { ESLintUtils } from '@typescript-eslint/utils'
import { createSyncFn } from 'synckit'
import type { RuleListener } from '@typescript-eslint/utils/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'))
import { syncAction } from './_'

export default ESLintUtils.RuleCreator(name => name)({
name: 'order',
Expand All @@ -28,7 +24,7 @@ export default ESLintUtils.RuleCreator(name => name)({
if (typeof node.value !== 'string' || !node.value.trim())
return
const input = node.value
const sorted = sortClasses(input).trim()
const sorted = syncAction('sort', input).trim()
if (sorted !== input) {
context.report({
node,
Expand Down
20 changes: 0 additions & 20 deletions packages/eslint-plugin/src/worker-sort.ts

This file was deleted.

44 changes: 44 additions & 0 deletions packages/eslint-plugin/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { loadConfig } from '@unocss/config'
import type { UnoGenerator } from '@unocss/core'
import { createGenerator } from '@unocss/core'
import { runAsWorker } from 'synckit'
import { sortRules } from '../../shared-integration/src/sort-rules'

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

async function _getGenerator() {
const { config, sources } = await loadConfig()
if (!sources.length)
throw new Error('[@unocss/eslint-plugin] No config file found, create a `uno.config.ts` file in your project root and try again.')
return createGenerator(config)
}

export async function getGenerator() {
promise = promise || _getGenerator()
return await promise
}

async function actionSort(classes: string) {
return await sortRules(classes, await getGenerator())
}

async function actionBlocklist(classes: string, id?: string) {
const uno = await getGenerator()
const extracted = await uno.applyExtractors(classes, id)
return [...extracted.values()].filter(i => uno.isBlocked(i))
}

export function run(action: 'sort', classes: string): string
export function run(action: 'blocklist', classes: string, id?: string): string[]
export function run(action: string, ...args: any[]): any {
switch (action) {
case 'sort':
// @ts-expect-error cast
return actionSort(...args)
case 'blocklist':
// @ts-expect-error cast
return actionBlocklist(...args)
}
}

runAsWorker(run as any)

0 comments on commit 0956f35

Please sign in to comment.