Skip to content

Commit

Permalink
feat(eslint-plugin): add new rule: enforce-class-compile (#3505) (#…
Browse files Browse the repository at this point in the history
…3506)

Co-authored-by: Jacob Bowdoin <7559478+jacob-8@users.noreply.github.com>
  • Loading branch information
levchak0910 and jacob-8 committed Mar 14, 2024
1 parent f3944de commit 25a938f
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 6 deletions.
29 changes: 24 additions & 5 deletions docs/integrations/eslint.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,43 @@ In legacy `.eslintrc` style:
- `@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/enforce-class-compile` - Enforce class compile [Optional].

### `@unocss/blocklist`
### Optional rules

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`:
These rules are not enabled by default. To enable it, add the following to your `.eslintrc`:

```json
{
"extends": [
"@unocss"
],
"rules": {
"@unocss/blocklist": "warn" // or "error"
"@unocss/<rule-name>": "warn", // or "error",
"@unocss/<another-rule-name>": ["warn" /* or "error" */, { /* options */ }]
}
}
```

#### `@unocss/blocklist`

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

#### `@unocss/enforce-class-compile` :wrench:

_This rule is designed to work in combination with [compile class transformer](https://unocss.dev/transformers/compile-class)._

Throw warning or error when class attribute or directive doesn't start with `:uno:`.

:wrench: automatically adds prefix `:uno:` to all class attributes and directives.

Options:

- `prefix` (string) - can be used in combination with [custom prefix](https://github.com/unocss/unocss/blob/main/packages/transformer-compile-class/src/index.ts#L34). Default: `:uno:`
- `enableFix` (boolean) - can be used for gradual migration when `false`. Default: `true`

**Note**: currently only Vue supported. _Contribute a PR_ if you want this in JSX. If you're looking for this in Svelte, you might be looking for [`svelte-scoped`](https://unocss.dev/integrations/svelte-scoped) mode.

## Prior Arts

Thanks to [eslint-plugin-unocss](https://github.com/devunt/eslint-plugin-unocss) by [@devunt](https://github.com/devunt).
17 changes: 17 additions & 0 deletions docs/transformers/compile-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ Will be compiled to:

You can config the trigger string and prefix for compile class with the options. Refer to [the types](https://github.com/unocss/unocss/blob/main/packages/transformer-compile-class/src/index.ts#L4) for details.

## Tooling

### ESLint

There is an eslint rule for enforcing the class compile transformer across the whole project: [@unocss/enforce-class-compile](https://unocss.dev/integrations/eslint#unocss-enforce-class-compile)

**Usage:**

```json
{
"plugins": ["@unocss"],
"rules": {
"@unocss/enforce-class-compile": "warn"
}
}
```

## License

- MIT License &copy; 2021-PRESENT [Anthony Fu](https://github.com/antfu)
3 changes: 2 additions & 1 deletion packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"synckit": "^0.9.0"
},
"devDependencies": {
"@unocss/eslint-plugin": "workspace:*"
"@unocss/eslint-plugin": "workspace:*",
"vue-eslint-parser": "^9.4.2"
}
}
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import orderAttributify from './rules/order-attributify'
import order from './rules/order'
import blocklist from './rules/blocklist'
import enforceClassCompile from './rules/enforce-class-compile'

export const plugin = {
rules: {
order,
'order-attributify': orderAttributify,
blocklist,
'enforce-class-compile': enforceClassCompile,
},
}
125 changes: 125 additions & 0 deletions packages/eslint-plugin/src/rules/enforce-class-compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { ESLintUtils } from '@typescript-eslint/utils'
import type { ReportFixFunction, RuleListener } from '@typescript-eslint/utils/ts-eslint'
import type { TSESTree } from '@typescript-eslint/types'
import type { AST } from 'vue-eslint-parser'
import { createRule } from './_'

export default createRule<[{ prefix: string, enableFix: boolean }], 'missing'>({
name: 'enforce-class-compile',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'Enforce class compilation',
},
messages: {
missing: 'prefix: `{{prefix}}` is missing',
},
schema: [{
type: 'object',
properties: {
prefix: {
type: 'string',
},
enableFix: {
type: 'boolean',
},
},
additionalProperties: false,
}],
},
defaultOptions: [{ prefix: ':uno:', enableFix: true }],
create(context, [mergedOptions]) {
const CLASS_COMPILE_PREFIX = `${mergedOptions.prefix} `
const ENABLE_FIX = mergedOptions.enableFix

function report({ node, fix }: { node: AST.VNode | AST.ESLintNode, fix: ReportFixFunction }) {
context.report({
node: node as unknown as TSESTree.Node,
loc: node.loc,
messageId: 'missing',
data: { prefix: CLASS_COMPILE_PREFIX.trim() },
fix: (...args) => ENABLE_FIX ? fix(...args) : null,
})
}

const scriptVisitor: RuleListener = {
JSXAttribute(_node) {
// todo: add support | NEED HELP
},
SvelteAttribute(_node: any) {
// todo: add support | NEED HELP
},
}

const reportClassList = (node: AST.VNode | AST.ESLintNode, classList: string) => {
if (classList.startsWith(CLASS_COMPILE_PREFIX))
return

report({
node,
fix(fixer) {
return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], `${CLASS_COMPILE_PREFIX}${classList}`)
},
})
}

const templateBodyVisitor: RuleListener = {
[`VAttribute[key.name=class]`](attr: AST.VAttribute) {
const valueNode = attr.value
if (!valueNode || !valueNode.value)
return

reportClassList(valueNode, valueNode.value)
},
[`VAttribute[key.argument.name=class] VExpressionContainer Literal:not(ConditionalExpression .test Literal):not(Property .value Literal)`](
literal: AST.ESLintStringLiteral,
) {
if (!literal.value || typeof literal.value !== 'string')
return

reportClassList(literal, literal.value)
},
[`VAttribute[key.argument.name=class] VExpressionContainer TemplateElement`](
templateElement: AST.ESLintTemplateElement,
) {
if (!templateElement.value.raw)
return

reportClassList(templateElement, templateElement.value.raw)
},
[`VAttribute[key.argument.name=class] VExpressionContainer Property`](
property: AST.ESLintProperty,
) {
if (property.key.type !== 'Identifier')
return

const classListString = property.key.name
if (classListString.startsWith(CLASS_COMPILE_PREFIX))
return

report({
node: property.key,
fix(fixer) {
let replacePropertyKeyText = `'${CLASS_COMPILE_PREFIX}${classListString}'`

if (property.shorthand)
replacePropertyKeyText = `${replacePropertyKeyText}: ${classListString}`

return fixer.replaceTextRange(property.key.range, replacePropertyKeyText)
},
})
},
}

// @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)
}
},
}) as any as ESLintUtils.RuleWithMeta<[], ''>

0 comments on commit 25a938f

Please sign in to comment.