Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prevent export of an initialized store #46

Merged
merged 2 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
}
]
}
]
})