Skip to content

Commit f241afa

Browse files
committed
feat: introduce eslint
1 parent c0b310f commit f241afa

25 files changed

+1405
-46
lines changed

.vscode/settings.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"editor.codeActionsOnSave": {
3-
"source.fixAll.eslint": "explicit",
4-
"source.fixAll.stylelint": "explicit"
3+
"source.fixAll.eslint": "explicit"
54
}
65
}

docs/.vitepress/config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export default defineConfig({
3333
text: 'Migration',
3434
link: `/introduction/migration`,
3535
},
36+
{
37+
text: 'ESLint',
38+
link: `/introduction/eslint`,
39+
},
3640
],
3741
},
3842
{
@@ -59,7 +63,7 @@ export default defineConfig({
5963
],
6064
},
6165
markdown: {
62-
languages: ['js', 'ts'],
66+
languages: ['js', 'ts', 'tsx'],
6367
codeTransformers: [
6468
transformerTwoslash({
6569
twoslasher: createTwoslasher({

docs/index.md

+3
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,8 @@ features:
3333
- icon: 🌈
3434
title: Hot Module Replacement
3535
details: Support functional components or defined by defineComponent.
36+
- icon: ⚙️
37+
title: ESLint
38+
details: Provide an ESLint plugin for vue-jsx-vapor to automatically fix code.
3639
---
3740

docs/introduction/eslint.md

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# ESLint
2+
3+
This is an ESLint plugin for `vue-jsx-vapor` to automatically fix code.
4+
5+
## Install
6+
7+
```sh
8+
pnpm add @vue-jsx-vapor/eslint
9+
```
10+
11+
## Setup
12+
13+
```ts
14+
// eslint.config.ts
15+
import vueJsxVapor from '@vue-jsx-vapor/eslint'
16+
17+
export default [
18+
vueJsxVapor()
19+
]
20+
```
21+
22+
## define-style
23+
24+
Use `prettier` to format styles in the defineStyle macro.
25+
26+
```ts twoslash
27+
import vueJsxVapor from '@vue-jsx-vapor/eslint'
28+
29+
export default [
30+
vueJsxVapor({
31+
rules: {
32+
'vue-jsx-vapor/define-style': ['error', { tabWidth: 2 }]
33+
}
34+
})
35+
]
36+
```
37+
38+
## jsx-sort-props
39+
40+
This is a modified version of [@stylistic/jsx/jsx-sort-props](https://eslint.style/rules/jsx/jsx-sort-props), supporting custom reservedFirst and reservedLast options.
41+
42+
```ts twoslash
43+
import vueJsxVapor from '@vue-jsx-vapor/eslint'
44+
45+
export default [
46+
vueJsxVapor({
47+
rules: {
48+
'vue-jsx-vapor/jsx-sort-props': ['error', {
49+
reservedFirst: ['v-if', 'v-for'],
50+
reservedLast: ['v-slot'],
51+
}]
52+
}
53+
})
54+
]
55+
```
56+
57+
### `reservedFirst`
58+
59+
Defaults to `['v-if', 'v-else-if', 'v-else', 'v-for', 'key', 'ref', 'v-model']`
60+
61+
If given as an array, the array's values will override the default list of reserved props.
62+
These props will respect the order specified in the array:
63+
64+
```jsx
65+
// before
66+
const Before = <App a v-for={i in list} v-if={list} b />
67+
68+
// after
69+
const After = <App v-if={list} v-for={i in list} a b />
70+
```
71+
72+
### `reservedLast`
73+
74+
Defaults to `['v-text', 'v-html', 'v-slots', 'v-slot']`
75+
76+
This can be an array option. These props must be listed after all other props.
77+
These will respect the order specified in the array:
78+
79+
```jsx
80+
// before
81+
const Before = <App v-slot={{ foo }} onClick={onClick} />
82+
83+
// after
84+
const After = <App onClick={onClick} v-slot={{ foo }} />
85+
```

docs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"preview": "vitepress preview"
99
},
1010
"dependencies": {
11+
"@vue-jsx-vapor/eslint": "workspace:*",
1112
"vue": "catalog:",
1213
"vue-jsx-vapor": "workspace:*"
1314
},

eslint.config.js

-21
This file was deleted.

eslint.config.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { sxzz } from '@sxzz/eslint-config'
2+
import vueJsxVapor from './packages/eslint/src/index'
3+
4+
export default [
5+
...(await sxzz()
6+
.removeRules(
7+
'unicorn/filename-case',
8+
'import/no-default-export',
9+
'unicorn/no-new-array',
10+
)
11+
.append([
12+
{
13+
name: 'docs',
14+
files: ['**/*.md/*.tsx'],
15+
rules: {
16+
'no-var': 'off',
17+
'no-mutable-exports': 'off',
18+
'no-duplicate-imports': 'off',
19+
'import/first': 'off',
20+
'unused-imports/no-unused-vars': 'off',
21+
},
22+
},
23+
])),
24+
vueJsxVapor({
25+
ignores: ['**/docs/**'],
26+
}),
27+
]

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"lint": "eslint .",
3232
"play": "npm -C playground run dev",
3333
"test": "vitest",
34-
"release": "pnpm lint && bumpp -r --all -x 'pnpm run changelog'",
34+
"release": "bumpp -r --all -x 'pnpm run changelog'",
3535
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
3636
"docs:dev": "pnpm run -C ./docs dev",
3737
"docs:preview": "pnpm run -C ./docs preview",

packages/eslint/package.json

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "@vue-jsx-vapor/eslint",
3+
"version": "2.2.0",
4+
"packageManager": "pnpm@10.4.1",
5+
"description": "Vue JSX Vapor ESLint Plugin",
6+
"type": "module",
7+
"keywords": [
8+
"vue",
9+
"jsx",
10+
"vapor",
11+
"eslint"
12+
],
13+
"license": "MIT",
14+
"homepage": "https://github.com/vuejs/vue-jsx-vapor#readme",
15+
"bugs": {
16+
"url": "https://github.com/vuejs/vue-jsx-vapor/issues"
17+
},
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/vuejs/vue-jsx-vapor.git"
21+
},
22+
"files": [
23+
"dist"
24+
],
25+
"main": "dist/index.cjs",
26+
"module": "dist/index.js",
27+
"types": "dist/index.d.ts",
28+
"exports": {
29+
".": {
30+
"dev": "./src/index.ts",
31+
"require": "./dist/index.cjs",
32+
"import": "./dist/index.js"
33+
},
34+
"./*": "./*"
35+
},
36+
"typesVersions": {
37+
"*": {
38+
"*": [
39+
"./dist/*",
40+
"./*"
41+
]
42+
}
43+
},
44+
"publishConfig": {
45+
".": {
46+
"require": "./dist/index.cjs",
47+
"import": "./dist/index.js"
48+
},
49+
"./*": "./*"
50+
},
51+
"scripts": {
52+
"build": "tsup",
53+
"dev": "DEV=true tsup",
54+
"release": "bumpp && npm publish",
55+
"test": "vitest"
56+
},
57+
"dependencies": {
58+
"@prettier/sync": "^0.5.5"
59+
},
60+
"devDependencies": {
61+
"@typescript-eslint/utils": "^8.29.1",
62+
"eslint-vitest-rule-tester": "^2.2.0"
63+
}
64+
}

packages/eslint/src/index.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import rules, { type Rules } from './rules'
2+
import type { Linter } from 'eslint'
3+
4+
export const plugins = {
5+
'vue-jsx-vapor': {
6+
rules,
7+
},
8+
}
9+
10+
export { rules, type Rules }
11+
12+
export default ({ rules = {}, ...options }: Linter.Config<Rules> = {}) => ({
13+
name: 'vue-jsx-vapor',
14+
plugins,
15+
rules: {
16+
'style/jsx-sort-props': 'off',
17+
'react/jsx-sort-props': 'off',
18+
'vue-jsx-vapor/jsx-sort-props': rules['vue-jsx-vapor/jsx-sort-props'] || [
19+
'warn',
20+
{
21+
callbacksLast: true,
22+
shorthandFirst: true,
23+
reservedFirst: [
24+
'v-if',
25+
'v-else-if',
26+
'v-else',
27+
'v-for',
28+
'key',
29+
'ref',
30+
'v-model',
31+
],
32+
reservedLast: ['v-text', 'v-html', 'v-slots', 'v-slot'],
33+
},
34+
],
35+
'vue-jsx-vapor/define-style': rules['vue-jsx-vapor/define-style'] || 'warn',
36+
} satisfies Rules & Record<string, unknown>,
37+
...options,
38+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import prettier from '@prettier/sync'
2+
import type { MessageIds, RuleOptions } from './types'
3+
import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'
4+
5+
const rule: RuleModule<MessageIds, RuleOptions> = {
6+
defaultOptions: [
7+
{
8+
tabWidth: 2,
9+
},
10+
],
11+
meta: {
12+
type: 'layout',
13+
docs: {
14+
description: 'Enforce consistent formatting in defineStyle CSS',
15+
},
16+
fixable: 'code',
17+
messages: {
18+
'define-style': 'Style in defineStyle should be properly formatted',
19+
'define-style-syntax-error': 'Syntax error in defineStyle',
20+
},
21+
schema: [
22+
{
23+
type: 'object',
24+
properties: {
25+
tabWidth: {
26+
type: 'number',
27+
default: 2,
28+
},
29+
},
30+
},
31+
],
32+
},
33+
create(context) {
34+
const configuration = context.options[0] || {}
35+
const tabWidth = configuration.tabWidth || 2
36+
return {
37+
CallExpression(node) {
38+
const callee =
39+
node.callee.type === 'MemberExpression'
40+
? node.callee.object
41+
: node.callee
42+
const offset = callee.loc.start.column
43+
const parser =
44+
node.callee.type === 'MemberExpression' &&
45+
node.callee.property.type === 'Identifier'
46+
? node.callee.property.name
47+
: 'css'
48+
if (callee.type === 'Identifier' && callee.name === 'defineStyle') {
49+
const arg = node.arguments[0]
50+
51+
if (arg?.type === 'TemplateLiteral') {
52+
const cssRaw = arg.quasis[0].value.raw
53+
54+
let formattedCss = ''
55+
try {
56+
formattedCss = prettier.format(cssRaw, { parser, tabWidth })
57+
} catch {
58+
return context.report({
59+
node: arg,
60+
messageId: 'define-style-syntax-error',
61+
})
62+
}
63+
64+
const placeholder = ' '.repeat(offset + tabWidth)
65+
const result = `\n${placeholder}${formattedCss.slice(0, -1).replaceAll('\n', `\n${placeholder}`)}\n${' '.repeat(offset)}`
66+
if (result !== cssRaw) {
67+
context.report({
68+
node: arg,
69+
messageId: 'define-style',
70+
fix(fixer) {
71+
return fixer.replaceTextRange(
72+
[arg.range[0] + 1, arg.range[1] - 1],
73+
result,
74+
)
75+
},
76+
})
77+
}
78+
}
79+
}
80+
},
81+
}
82+
},
83+
}
84+
export default rule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface DefineStyleSchema0 {
2+
tabWidth?: number
3+
}
4+
5+
export type DefineStyleRuleOptions = [DefineStyleSchema0?]
6+
7+
export type RuleOptions = DefineStyleRuleOptions
8+
export type MessageIds = 'define-style' | 'define-style-syntax-error'

0 commit comments

Comments
 (0)