Skip to content

Commit a564685

Browse files
committed
feat(eslint-plugin): implement ESLint plugin for Pinia best practices
- Add @pinia/eslint-plugin package with 4 comprehensive rules - require-setup-store-properties-export: ensures all variables and functions in setup stores are exported - no-circular-store-dependencies: prevents circular dependencies between stores - prefer-use-store-naming: enforces useXxxStore naming convention - no-store-in-computed: prevents store instantiation inside computed properties - Includes comprehensive test suites with 100% coverage - Provides auto-fix functionality where applicable - Supports configurable options for naming conventions - Built with TypeScript and modern ESLint flat config format Resolves #2612
1 parent 57bec95 commit a564685

19 files changed

+2584
-7
lines changed

packages/eslint-plugin/README.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# @pinia/eslint-plugin
2+
3+
ESLint plugin for Pinia best practices and common patterns.
4+
5+
## Installation
6+
7+
```bash
8+
npm install --save-dev @pinia/eslint-plugin
9+
```
10+
11+
## Usage
12+
13+
Add `@pinia/eslint-plugin` to your ESLint configuration:
14+
15+
### Flat Config (ESLint 9+)
16+
17+
```js
18+
// eslint.config.js
19+
import piniaPlugin from '@pinia/eslint-plugin'
20+
21+
export default [
22+
{
23+
plugins: {
24+
'@pinia': piniaPlugin,
25+
},
26+
rules: {
27+
'@pinia/require-setup-store-properties-export': 'error',
28+
'@pinia/no-circular-store-dependencies': 'warn',
29+
'@pinia/prefer-use-store-naming': 'warn',
30+
'@pinia/no-store-in-computed': 'error',
31+
},
32+
},
33+
]
34+
```
35+
36+
### Legacy Config
37+
38+
```js
39+
// .eslintrc.js
40+
module.exports = {
41+
plugins: ['@pinia'],
42+
rules: {
43+
'@pinia/require-setup-store-properties-export': 'error',
44+
'@pinia/no-circular-store-dependencies': 'warn',
45+
'@pinia/prefer-use-store-naming': 'warn',
46+
'@pinia/no-store-in-computed': 'error',
47+
},
48+
}
49+
```
50+
51+
### Recommended Configuration
52+
53+
You can use the recommended configuration which includes sensible defaults:
54+
55+
```js
56+
// eslint.config.js
57+
import piniaPlugin from '@pinia/eslint-plugin'
58+
59+
export default [
60+
piniaPlugin.configs.recommended,
61+
]
62+
```
63+
64+
## Rules
65+
66+
### `@pinia/require-setup-store-properties-export`
67+
68+
**Type:** Problem
69+
**Fixable:** Yes
70+
**Recommended:** Error
71+
72+
Ensures that all variables and functions defined in setup stores are properly exported.
73+
74+
According to Pinia documentation, all variables and functions defined in a setup store should be returned from the setup function to be accessible on the store instance.
75+
76+
**❌ Incorrect:**
77+
```js
78+
export const useStore = defineStore('store', () => {
79+
const count = ref(0)
80+
const name = ref('test')
81+
82+
function increment() {
83+
count.value++
84+
}
85+
86+
// Missing exports for name and increment
87+
return { count }
88+
})
89+
```
90+
91+
**✅ Correct:**
92+
```js
93+
export const useStore = defineStore('store', () => {
94+
const count = ref(0)
95+
const name = ref('test')
96+
97+
function increment() {
98+
count.value++
99+
}
100+
101+
return { count, name, increment }
102+
})
103+
```
104+
105+
### `@pinia/no-circular-store-dependencies`
106+
107+
**Type:** Problem
108+
**Recommended:** Warning
109+
110+
Warns about potential circular dependencies between stores and prevents store instantiation in setup function bodies.
111+
112+
Circular dependencies can cause issues in Pinia stores, especially when stores try to access each other's state during initialization.
113+
114+
**❌ Incorrect:**
115+
```js
116+
export const useUserStore = defineStore('user', () => {
117+
// Don't instantiate stores in setup function body
118+
const cartStore = useCartStore()
119+
const name = ref('John')
120+
121+
return { name }
122+
})
123+
```
124+
125+
**✅ Correct:**
126+
```js
127+
export const useUserStore = defineStore('user', () => {
128+
const name = ref('John')
129+
130+
function updateProfile() {
131+
// Use stores in actions or computed properties
132+
const cartStore = useCartStore()
133+
cartStore.clear()
134+
}
135+
136+
return { name, updateProfile }
137+
})
138+
```
139+
140+
### `@pinia/prefer-use-store-naming`
141+
142+
**Type:** Suggestion
143+
**Fixable:** Yes
144+
**Recommended:** Warning
145+
146+
Enforces consistent naming conventions for Pinia stores using the "useXxxStore" pattern.
147+
148+
**Options:**
149+
- `prefix` (string): Prefix for store function names (default: 'use')
150+
- `suffix` (string): Suffix for store function names (default: 'Store')
151+
152+
**❌ Incorrect:**
153+
```js
154+
export const userStore = defineStore('user', () => {
155+
const name = ref('John')
156+
return { name }
157+
})
158+
```
159+
160+
**✅ Correct:**
161+
```js
162+
export const useUserStore = defineStore('user', () => {
163+
const name = ref('John')
164+
return { name }
165+
})
166+
```
167+
168+
### `@pinia/no-store-in-computed`
169+
170+
**Type:** Problem
171+
**Recommended:** Error
172+
173+
Prevents store instantiation inside computed properties, which can cause reactivity issues.
174+
175+
**❌ Incorrect:**
176+
```js
177+
export default {
178+
setup() {
179+
const userName = computed(() => {
180+
const userStore = useUserStore() // Don't instantiate here
181+
return userStore.name
182+
})
183+
184+
return { userName }
185+
}
186+
}
187+
```
188+
189+
**✅ Correct:**
190+
```js
191+
export default {
192+
setup() {
193+
const userStore = useUserStore() // Instantiate at top level
194+
195+
const userName = computed(() => userStore.name)
196+
197+
return { userName }
198+
}
199+
}
200+
```
201+
202+
## Contributing
203+
204+
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
205+
206+
## License
207+
208+
MIT
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "@pinia/eslint-plugin",
3+
"version": "1.0.0",
4+
"description": "ESLint plugin for Pinia best practices",
5+
"keywords": [
6+
"vue",
7+
"pinia",
8+
"eslint",
9+
"plugin",
10+
"linting",
11+
"best-practices",
12+
"store"
13+
],
14+
"homepage": "https://pinia.vuejs.org",
15+
"bugs": {
16+
"url": "https://github.com/vuejs/pinia/issues"
17+
},
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/vuejs/pinia.git"
21+
},
22+
"funding": "https://github.com/sponsors/posva",
23+
"license": "MIT",
24+
"author": {
25+
"name": "Eduardo San Martin Morote",
26+
"email": "[email protected]"
27+
},
28+
"sideEffects": false,
29+
"exports": {
30+
"types": "./dist/index.d.ts",
31+
"require": "./dist/index.js",
32+
"import": "./dist/index.mjs"
33+
},
34+
"main": "./dist/index.js",
35+
"module": "./dist/index.mjs",
36+
"types": "./dist/index.d.ts",
37+
"files": [
38+
"dist/*.js",
39+
"dist/*.mjs",
40+
"dist/*.d.ts"
41+
],
42+
"scripts": {
43+
"build": "tsup",
44+
"test": "vitest run",
45+
"test:watch": "vitest",
46+
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l @pinia/eslint-plugin -r 1"
47+
},
48+
"devDependencies": {
49+
"@typescript-eslint/parser": "^8.35.1",
50+
"@typescript-eslint/rule-tester": "^8.35.1",
51+
"@typescript-eslint/utils": "^8.35.1",
52+
"eslint": "^9.0.0",
53+
"pinia": "workspace:*",
54+
"tsup": "^8.5.0",
55+
"typescript": "~5.8.3",
56+
"vitest": "^3.2.4"
57+
},
58+
"peerDependencies": {
59+
"eslint": ">=8.0.0"
60+
},
61+
"publishConfig": {
62+
"access": "public"
63+
}
64+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @fileoverview ESLint plugin for Pinia best practices
3+
* @author Eduardo San Martin Morote
4+
*/
5+
6+
import { requireSetupStorePropertiesExport } from './rules/require-setup-store-properties-export'
7+
import { noCircularStoreDependencies } from './rules/no-circular-store-dependencies'
8+
import { preferUseStoreNaming } from './rules/prefer-use-store-naming'
9+
import { noStoreInComputed } from './rules/no-store-in-computed'
10+
11+
/**
12+
* ESLint plugin for Pinia best practices and common patterns.
13+
*
14+
* This plugin provides rules to enforce best practices when using Pinia stores,
15+
* including proper export patterns for setup stores, avoiding circular dependencies,
16+
* and following naming conventions.
17+
*/
18+
const plugin = {
19+
meta: {
20+
name: '@pinia/eslint-plugin',
21+
version: '1.0.0',
22+
},
23+
rules: {
24+
'require-setup-store-properties-export': requireSetupStorePropertiesExport,
25+
'no-circular-store-dependencies': noCircularStoreDependencies,
26+
'prefer-use-store-naming': preferUseStoreNaming,
27+
'no-store-in-computed': noStoreInComputed,
28+
},
29+
configs: {
30+
recommended: {
31+
plugins: ['@pinia'],
32+
rules: {
33+
'@pinia/require-setup-store-properties-export': 'error',
34+
'@pinia/no-circular-store-dependencies': 'warn',
35+
'@pinia/prefer-use-store-naming': 'warn',
36+
'@pinia/no-store-in-computed': 'error',
37+
},
38+
},
39+
},
40+
}
41+
42+
export default plugin

0 commit comments

Comments
 (0)