Skip to content

Commit

Permalink
feat: prevent export of an initialized store (#46)
Browse files Browse the repository at this point in the history
* feat: prevent export of an initialized store

* test: corrected export in invalid example
  • Loading branch information
pjfontillas authored Jul 5, 2024
1 parent c0a096f commit a595837
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 9 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ To use the all configuration, extend it in your `.eslintrc` file:
🌐 Set in the `all` configuration.\
✅ Set in the `recommended` configuration.

| Name                                  | Description | 💼 | ⚠️ | 🚫 |
| :------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :- | :- | :- |
| [no-duplicate-store-ids](docs/rules/no-duplicate-store-ids.md) | Disallow duplicate store ids || 🌐 | |
| [no-return-global-properties](docs/rules/no-return-global-properties.md) | Disallows returning globally provided properties from Pinia stores. || 🌐 | |
| [prefer-single-store-per-file](docs/rules/prefer-single-store-per-file.md) | Encourages defining each store in a separate file. | | | 🌐 |
| [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. | | 🌐 | |
| [require-setup-store-properties-export](docs/rules/require-setup-store-properties-export.md) | In setup stores all state properties must be exported. || 🌐 | |
| Name                                  | Description | 💼 | ⚠️ | 🚫 |
| :------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :--- | :- | :- |
| [never-export-initialized-store](docs/rules/never-export-initialized-store.md) | Never export an initialized named or default store. | 🌐 ✅ | | |
| [no-duplicate-store-ids](docs/rules/no-duplicate-store-ids.md) | Disallow duplicate store ids. || 🌐 | |
| [no-return-global-properties](docs/rules/no-return-global-properties.md) | Disallows returning globally provided properties from Pinia stores. || 🌐 | |
| [prefer-single-store-per-file](docs/rules/prefer-single-store-per-file.md) | Encourages defining each store in a separate file. | | | 🌐 |
| [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. | | 🌐 | |
| [require-setup-store-properties-export](docs/rules/require-setup-store-properties-export.md) | In setup stores all state properties must be exported. || 🌐 | |

<!-- end auto-generated rules list -->

Expand All @@ -101,4 +102,4 @@ This project exists thanks to all the people who contribute 😍!

<a href="https://github.com/lisilinhart/eslint-plugin-pinia/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lisilinhart/eslint-plugin-pinia" />
</a>
</a>
60 changes: 60 additions & 0 deletions docs/rules/never-export-initialized-store.md
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.
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import requireSetupStoreProps, {
RULE_NAME as requireSetupStorePropsName
} from './rules/require-setup-store-properties-export'

import neverExportInitializedStore, {
RULE_NAME as neverExportInitializedStoreName
} from './rules/never-export-initialized-store'

import preferNamingConvention, {
RULE_NAME as preferNamingConventionName
} from './rules/prefer-use-store-naming-convention'
Expand Down Expand Up @@ -30,6 +34,7 @@ const createConfig = (rules: Record<string, string>) => ({

const allRules = {
[requireSetupStorePropsName]: 'warn',
[neverExportInitializedStoreName]: 'error',
[preferNamingConventionName]: 'warn',
[preferSingleStoreName]: 'off',
[noReturnGlobalPropertiesName]: 'warn',
Expand All @@ -39,12 +44,14 @@ const allRules = {
const recommended = {
[requireSetupStorePropsName]: 'error',
[noReturnGlobalPropertiesName]: 'error',
[noDuplicateStoreIdsName]: 'error'
[noDuplicateStoreIdsName]: 'error',
[neverExportInitializedStoreName]: 'error'
}

export default {
rules: {
[requireSetupStorePropsName]: requireSetupStoreProps,
[neverExportInitializedStoreName]: neverExportInitializedStore,
[preferNamingConventionName]: preferNamingConvention,
[preferSingleStoreName]: preferSingleStore,
[noReturnGlobalPropertiesName]: noReturnGlobalProperties,
Expand Down
80 changes: 80 additions & 0 deletions src/rules/never-export-initialized-store.ts
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
}
})
}
})
}
}
}
}
})
48 changes: 48 additions & 0 deletions tests/rules/never-export-initialized-store.ts
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'
}
]
}
]
})

0 comments on commit a595837

Please sign in to comment.