diff --git a/README.md b/README.md index f2154c1..2ea5932 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ export default [ | [never-export-initialized-store](docs/rules/never-export-initialized-store.md) | Never export an initialized named or default store. | ✅ ![badge-recommended-flat][] | 🌐 ![badge-all-flat][] | | | [no-duplicate-store-ids](docs/rules/no-duplicate-store-ids.md) | Disallow duplicate store ids. | ✅ ![badge-recommended-flat][] | 🌐 ![badge-all-flat][] | | | [no-return-global-properties](docs/rules/no-return-global-properties.md) | Disallows returning globally provided properties from Pinia stores. | ✅ ![badge-recommended-flat][] | 🌐 ![badge-all-flat][] | | +| [no-store-to-refs-in-store](docs/rules/no-store-to-refs-in-store.md) | Disallow use of storeToRefs inside defineStore | ✅ ![badge-recommended-flat][] | 🌐 ![badge-all-flat][] | | | [prefer-single-store-per-file](docs/rules/prefer-single-store-per-file.md) | Encourages defining each store in a separate file. | | | 🌐 ![badge-all-flat][] | | [prefer-use-store-naming-convention](docs/rules/prefer-use-store-naming-convention.md) | Enforces the convention of naming stores with the prefix `use` followed by the store name. | | 🌐 ✅ ![badge-all-flat][] ![badge-recommended-flat][] | | | [require-setup-store-properties-export](docs/rules/require-setup-store-properties-export.md) | In setup stores all state properties must be exported. | ✅ ![badge-recommended-flat][] | 🌐 ![badge-all-flat][] | | diff --git a/docs/rules/no-store-to-refs-in-store.md b/docs/rules/no-store-to-refs-in-store.md new file mode 100644 index 0000000..baa2b5a --- /dev/null +++ b/docs/rules/no-store-to-refs-in-store.md @@ -0,0 +1,52 @@ +# Disallow use of storeToRefs inside defineStore (`pinia/no-store-to-refs-in-store`) + +💼⚠️ This rule is enabled in the following configs: ✅ `recommended`, `recommended-flat`. This rule _warns_ in the following configs: 🌐 `all`, `all-flat`. + + + +## Rule Details + +When stores are cross used, whichever store gets its use... called first will exists as a placeholder in the other store until its own setup function returns. That's why storeToRefs() do not work there and should be avoided altogether with cross used stores. + +❌ Examples of **incorrect** code for this rule: + +```js +import { useUserStore } from './user' + +export const useCartStore = defineStore('cart', () => { + const { user } = storeToRefs(useUserStore()) + const list = ref([]) + + const summary = computed(() => { + return `Hi ${user.name}, you have ${list.value.length} items in your cart. It costs ${price.value}.` + }) + + function purchase() { + return apiPurchase(user.id, this.list) + } + + return { summary, purchase } +}) +``` + +✅ Examples of **correct** code for this rule: + +```js +import { useUserStore } from './user' + +export const useCartStore = defineStore('cart', () => { + const { user } = useUserStore() + const list = ref([]) + + const summary = computed(() => { + return `Hi ${user.name}, you have ${list.value.length} items in your cart. It costs ${price.value}.` + }) + + function purchase() { + return apiPurchase(user.id, this.list) + } + + return { summary, purchase } +}) + +``` diff --git a/src/index.ts b/src/index.ts index 1f13314..1e30357 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { RULE_NAME as preferNamingConventionName } from './rules/prefer-use-stor import { RULE_NAME as preferSingleStoreName } from './rules/prefer-single-store-per-file' import { RULE_NAME as noReturnGlobalPropertiesName } from './rules/no-return-global-properties' import { RULE_NAME as noDuplicateStoreIdsName } from './rules/no-duplicate-store-ids' +import { RULE_NAME as noStoreToRefs } from './rules/no-store-to-refs-in-store' import rules from './rules/index' const plugin = { @@ -11,23 +12,28 @@ const plugin = { } const allRules = { - [requireSetupStorePropsName]: 'warn', [neverExportInitializedStoreName]: 'warn', + [noDuplicateStoreIdsName]: 'warn', + [noReturnGlobalPropertiesName]: 'warn', + [noStoreToRefs]: 'warn', [preferNamingConventionName]: 'warn', [preferSingleStoreName]: 'off', - [noReturnGlobalPropertiesName]: 'warn', - [noDuplicateStoreIdsName]: 'warn' + [requireSetupStorePropsName]: 'warn' } const recommended = { - [requireSetupStorePropsName]: 'error', - [noReturnGlobalPropertiesName]: 'error', - [noDuplicateStoreIdsName]: 'error', [neverExportInitializedStoreName]: 'error', - [preferNamingConventionName]: 'warn' + [noDuplicateStoreIdsName]: 'error', + [noReturnGlobalPropertiesName]: 'error', + [noStoreToRefs]: 'error', + [preferNamingConventionName]: 'warn', + [requireSetupStorePropsName]: 'error' } -function createConfig>(_rules: T, flat = false) { +function createConfig>( + _rules: T, + flat = false +) { const name = 'pinia' const constructedRules: Record<`pinia/${string}`, string> = Object.keys( _rules diff --git a/src/rules/index.ts b/src/rules/index.ts index 7092d55..d8d7a07 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -2,6 +2,7 @@ import neverExportInitializedStore, { RULE_NAME as neverExportInitializedStoreName } from './never-export-initialized-store' import noDuplicateStoreIds, { RULE_NAME as noDuplicateStoreIdsName } from './no-duplicate-store-ids' import noReturnGlobalProperties, { RULE_NAME as noReturnGlobalPropertiesName } from './no-return-global-properties' +import noStoreToRefsInStore, { RULE_NAME as noStoreToRefsInStoreName } from './no-store-to-refs-in-store' import preferSingleStorePerFile, { RULE_NAME as preferSingleStorePerFileName } from './prefer-single-store-per-file' import preferUseStoreNamingConvention, { RULE_NAME as preferUseStoreNamingConventionName } from './prefer-use-store-naming-convention' import requireSetupStorePropertiesExport, { RULE_NAME as requireSetupStorePropertiesExportName } from './require-setup-store-properties-export' @@ -10,6 +11,7 @@ export default { [neverExportInitializedStoreName]: neverExportInitializedStore, [noDuplicateStoreIdsName]: noDuplicateStoreIds, [noReturnGlobalPropertiesName]: noReturnGlobalProperties, + [noStoreToRefsInStoreName]: noStoreToRefsInStore, [preferSingleStorePerFileName]: preferSingleStorePerFile, [preferUseStoreNamingConventionName]: preferUseStoreNamingConvention, [requireSetupStorePropertiesExportName]: requireSetupStorePropertiesExport, diff --git a/src/rules/no-duplicate-store-ids.ts b/src/rules/no-duplicate-store-ids.ts index bdbb3c0..d7a30ec 100644 --- a/src/rules/no-duplicate-store-ids.ts +++ b/src/rules/no-duplicate-store-ids.ts @@ -1,6 +1,6 @@ +import { resolve } from 'path' import { AST_NODE_TYPES } from '@typescript-eslint/utils' import { createEslintRule } from '../utils/rule-creator' -import { resolve } from 'path' export const RULE_NAME = 'no-duplicate-store-ids' export type MESSAGE_IDS = 'duplicatedStoreIds' diff --git a/src/rules/no-store-to-refs-in-store.ts b/src/rules/no-store-to-refs-in-store.ts new file mode 100644 index 0000000..281633b --- /dev/null +++ b/src/rules/no-store-to-refs-in-store.ts @@ -0,0 +1,62 @@ +import { createEslintRule } from '../utils/rule-creator' + +export const RULE_NAME = 'no-store-to-refs-in-store' +export type MESSAGE_IDS = 'storeToRefs' +type Options = [] + +export default createEslintRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow use of storeToRefs inside defineStore' + }, + schema: [], + messages: { + storeToRefs: + 'Do not use storeToRefs in other stores. Use the store as a whole directly.' + } + }, + defaultOptions: [], + create: (context) => { + return { + CallExpression(node) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'defineStore' && + node.arguments.length >= 2 + ) { + const functionBody = node.arguments[1] + if ( + functionBody.type === 'ArrowFunctionExpression' || + functionBody.type === 'FunctionExpression' + ) { + const body = functionBody.body + if (body.type === 'BlockStatement') { + body.body.forEach((statement) => { + if ( + statement.type === 'VariableDeclaration' && + statement.declarations.length > 0 + ) { + statement.declarations.forEach((declaration) => { + if ( + declaration.init && + declaration.init.type === 'CallExpression' && + declaration.init.callee.type === 'Identifier' && + declaration.init.callee.name === 'storeToRefs' + ) { + context.report({ + node: declaration.init.callee, + messageId: 'storeToRefs' + }) + } + }) + } + }) + } + } + } + } + } + } +}) diff --git a/src/rules/require-setup-store-properties-export.ts b/src/rules/require-setup-store-properties-export.ts index 9f8effc..c4c2f76 100644 --- a/src/rules/require-setup-store-properties-export.ts +++ b/src/rules/require-setup-store-properties-export.ts @@ -1,6 +1,6 @@ import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils' +import { isIdentifier } from '@typescript-eslint/utils/ast-utils' import { createEslintRule } from '../utils/rule-creator' -import { getPropertyName, isIdentifier } from '@typescript-eslint/utils/ast-utils' import { isRefOrReactiveCall } from '../utils/ast-utils' export const RULE_NAME = 'require-setup-store-properties-export' diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts index 731a2c9..c9738d5 100644 --- a/src/utils/ast-utils.ts +++ b/src/utils/ast-utils.ts @@ -1,10 +1,10 @@ -import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils"; - -export function isRefOrReactiveCall(node: TSESTree.Expression | null): node is TSESTree.CallExpression { - return ( - !!node && - node.type === AST_NODE_TYPES.CallExpression && - node.callee.type === 'Identifier' && - (node.callee.name === 'ref' || node.callee.name === 'reactive') - ) -} \ No newline at end of file +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils' + +export function isRefOrReactiveCall(node: TSESTree.Expression | null): node is TSESTree.CallExpression { + return ( + !!node && + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === 'Identifier' && + (node.callee.name === 'ref' || node.callee.name === 'reactive') + ) +} diff --git a/tests/fixtures/fixtures.test.ts b/tests/fixtures/fixtures.test.ts index 3442ec9..8d77be7 100644 --- a/tests/fixtures/fixtures.test.ts +++ b/tests/fixtures/fixtures.test.ts @@ -7,6 +7,6 @@ const target = resolve(__dirname, '..', 'fixtures') it.concurrent('fixtures/no-duplicate-store-ids', { fails: true }, async () => { await execa('npx', ['eslint', 'no-duplicate-store-ids'], { cwd: target, - stdio: 'pipe', + stdio: 'pipe' }) }) diff --git a/tests/rules/no-store-to-refs-in-store.test.ts b/tests/rules/no-store-to-refs-in-store.test.ts new file mode 100644 index 0000000..3008a39 --- /dev/null +++ b/tests/rules/no-store-to-refs-in-store.test.ts @@ -0,0 +1,42 @@ +import rule, { RULE_NAME } from '../../src/rules/no-store-to-refs-in-store' +import { ruleTester } from '../rule-tester' + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: `import { defineStore } from 'pinia' + export const useCounterStore = defineStore()` + }, + { + code: `import { defineStore } from 'pinia' +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + return { count } +}) + export const useTodoStore = defineStore('todo', () => { + const todo = ref(0) + return { todo } +})` + } + ], + invalid: [ + { + code: `import { defineStore } from 'pinia' + +const useCounterStore = defineStore('counter', () => { + const { user } = storeToRefs(useUserStore()) + + const count = ref(0) + return { count } +}) + +const useCounter2Store = defineStore('counter', () => { +})`, + errors: [ + { + messageId: 'storeToRefs' + } + ] + } + ] +})