-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: prevent export of an initialized store (#46)
* feat: prevent export of an initialized store * test: corrected export in invalid example
- Loading branch information
1 parent
c0a096f
commit a595837
Showing
5 changed files
with
205 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Never export an initialized named or default store (`pinia/never-export-initialized-store`) | ||
|
||
💼 This rule is enabled in the following configs: 🌐 `all`, ✅ `recommended`. | ||
|
||
<!-- end auto-generated rule header --> | ||
|
||
Here's the documentation for the `never-export-initialized-store` rule: | ||
|
||
## Rule Details | ||
|
||
This rule ensures that we never export an initialized store. | ||
|
||
❌ Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
// counter.js | ||
import { defineStore } from 'pinia'; | ||
|
||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
}); | ||
|
||
export const foo = useCounterStore(); | ||
``` | ||
|
||
```js | ||
// counter.js | ||
import { defineStore } from 'pinia'; | ||
|
||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
}); | ||
|
||
export default useCounterStore(); | ||
``` | ||
|
||
✅ Examples of **correct** code for this rule: | ||
|
||
```js | ||
// counter.js | ||
import { defineStore } from 'pinia'; | ||
|
||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
}); | ||
``` | ||
|
||
```js | ||
// app.vue | ||
import { useCounterStore } from './counter.js'; | ||
|
||
const store = useCounterStore(); | ||
``` | ||
|
||
Exporting store will cause unexpected results when application uses server side rendering. | ||
|
||
If multiple components import the same instance of useStore and modify the state, those changes will be reflected across all components because they share the same store instance. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils' | ||
import { createEslintRule } from '../utils/rule-creator' | ||
|
||
export const RULE_NAME = 'never-export-initialized-store' | ||
export type MESSAGE_IDS = | ||
| 'namedInitialization' | ||
| 'defaultInitialization' | ||
type Options = [] | ||
|
||
const storeIds = new Set<string>() | ||
export default createEslintRule<Options, MESSAGE_IDS>({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'Never export an initialized named or default store.' | ||
}, | ||
schema: [], | ||
messages: { | ||
namedInitialization: | ||
'Never export an initialized store: {{storeName}}. Use inject/import instead where it is used.', | ||
defaultInitialization: | ||
'Never export default initialized store. Use inject/import instead where it is used.' | ||
} | ||
}, | ||
defaultOptions: [], | ||
create: (context) => { | ||
return { | ||
CallExpression(node) { | ||
if ( | ||
node.callee.type === 'Identifier' && | ||
node.callee.name === 'defineStore' && | ||
node.arguments.length >= 2 && | ||
node.arguments[0].type === 'Literal' && | ||
typeof node.arguments[0].value === 'string' && | ||
node.parent.id.type === 'Identifier' | ||
) { | ||
const callee = node.callee | ||
if (callee.type !== 'Identifier' || callee.name !== 'defineStore') | ||
return | ||
|
||
const storeId = node.arguments && node.arguments[0] | ||
|
||
if (!storeId || storeId.type !== AST_NODE_TYPES.Literal) return | ||
|
||
const value = node.parent.id.name as string | ||
storeIds.add(value) | ||
} | ||
}, | ||
ExportDefaultDeclaration(node) { | ||
if ( | ||
storeIds.has(node.declaration?.parent?.declaration?.callee?.name) | ||
) { | ||
context.report({ | ||
node, | ||
messageId: 'defaultInitialization' | ||
}) | ||
} | ||
}, | ||
ExportNamedDeclaration(node) { | ||
if (node?.declaration?.type === 'VariableDeclaration') { | ||
node?.declaration?.declarations.forEach(declaration => { | ||
if ( | ||
storeIds.has(declaration?.init?.callee?.name) | ||
) { | ||
context.report({ | ||
node, | ||
messageId: 'namedInitialization', | ||
data: { | ||
storeName: declaration?.init?.callee?.name | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import rule, { RULE_NAME } from '../../src/rules/never-export-initialized-store' | ||
import { ruleTester } from '../rule-tester' | ||
|
||
ruleTester.run(RULE_NAME, rule, { | ||
valid: [ | ||
{ | ||
code: `import { defineStore } from 'pinia'; | ||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
});` | ||
} | ||
], | ||
invalid: [ | ||
{ | ||
code: `import { defineStore } from 'pinia'; | ||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
}); | ||
export const foo = useCounterStore();`, | ||
errors: [ | ||
{ | ||
messageId: 'namedInitialization' | ||
} | ||
] | ||
}, | ||
{ | ||
code: `import { defineStore } from 'pinia'; | ||
export const useCounterStore = defineStore('counter', () => { | ||
const count = ref(0); | ||
return { count }; | ||
}); | ||
export default useCounterStore(); | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'defaultInitialization' | ||
} | ||
] | ||
} | ||
] | ||
}) |