From a564685465fe732f22ad155f3769baa2473544e4 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Mon, 15 Sep 2025 09:31:47 +0300 Subject: [PATCH 1/7] 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 --- packages/eslint-plugin/README.md | 208 ++++++ packages/eslint-plugin/package.json | 64 ++ packages/eslint-plugin/src/index.ts | 42 ++ .../no-circular-store-dependencies.test.ts | 124 ++++ .../__tests__/no-store-in-computed.test.ts | 127 ++++ .../__tests__/prefer-use-store-naming.test.ts | 158 +++++ ...uire-setup-store-properties-export.test.ts | 170 +++++ .../rules/no-circular-store-dependencies.ts | 189 ++++++ .../src/rules/no-store-in-computed.ts | 111 ++++ .../src/rules/prefer-use-store-naming.ts | 111 ++++ .../require-setup-store-properties-export.ts | 132 ++++ packages/eslint-plugin/src/types.ts | 99 +++ packages/eslint-plugin/src/utils/ast-utils.ts | 116 ++++ .../eslint-plugin/src/utils/store-utils.ts | 95 +++ packages/eslint-plugin/test_output.txt | 190 ++++++ packages/eslint-plugin/tsconfig.build.json | 12 + packages/eslint-plugin/tsup.config.ts | 12 + packages/eslint-plugin/vitest.config.ts | 9 + pnpm-lock.yaml | 622 +++++++++++++++++- 19 files changed, 2584 insertions(+), 7 deletions(-) create mode 100644 packages/eslint-plugin/README.md create mode 100644 packages/eslint-plugin/package.json create mode 100644 packages/eslint-plugin/src/index.ts create mode 100644 packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts create mode 100644 packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts create mode 100644 packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts create mode 100644 packages/eslint-plugin/src/rules/__tests__/require-setup-store-properties-export.test.ts create mode 100644 packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts create mode 100644 packages/eslint-plugin/src/rules/no-store-in-computed.ts create mode 100644 packages/eslint-plugin/src/rules/prefer-use-store-naming.ts create mode 100644 packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts create mode 100644 packages/eslint-plugin/src/types.ts create mode 100644 packages/eslint-plugin/src/utils/ast-utils.ts create mode 100644 packages/eslint-plugin/src/utils/store-utils.ts create mode 100644 packages/eslint-plugin/test_output.txt create mode 100644 packages/eslint-plugin/tsconfig.build.json create mode 100644 packages/eslint-plugin/tsup.config.ts create mode 100644 packages/eslint-plugin/vitest.config.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 0000000000..7c4035f929 --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -0,0 +1,208 @@ +# @pinia/eslint-plugin + +ESLint plugin for Pinia best practices and common patterns. + +## Installation + +```bash +npm install --save-dev @pinia/eslint-plugin +``` + +## Usage + +Add `@pinia/eslint-plugin` to your ESLint configuration: + +### Flat Config (ESLint 9+) + +```js +// eslint.config.js +import piniaPlugin from '@pinia/eslint-plugin' + +export default [ + { + plugins: { + '@pinia': piniaPlugin, + }, + rules: { + '@pinia/require-setup-store-properties-export': 'error', + '@pinia/no-circular-store-dependencies': 'warn', + '@pinia/prefer-use-store-naming': 'warn', + '@pinia/no-store-in-computed': 'error', + }, + }, +] +``` + +### Legacy Config + +```js +// .eslintrc.js +module.exports = { + plugins: ['@pinia'], + rules: { + '@pinia/require-setup-store-properties-export': 'error', + '@pinia/no-circular-store-dependencies': 'warn', + '@pinia/prefer-use-store-naming': 'warn', + '@pinia/no-store-in-computed': 'error', + }, +} +``` + +### Recommended Configuration + +You can use the recommended configuration which includes sensible defaults: + +```js +// eslint.config.js +import piniaPlugin from '@pinia/eslint-plugin' + +export default [ + piniaPlugin.configs.recommended, +] +``` + +## Rules + +### `@pinia/require-setup-store-properties-export` + +**Type:** Problem +**Fixable:** Yes +**Recommended:** Error + +Ensures that all variables and functions defined in setup stores are properly exported. + +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. + +**❌ Incorrect:** +```js +export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + function increment() { + count.value++ + } + + // Missing exports for name and increment + return { count } +}) +``` + +**✅ Correct:** +```js +export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + function increment() { + count.value++ + } + + return { count, name, increment } +}) +``` + +### `@pinia/no-circular-store-dependencies` + +**Type:** Problem +**Recommended:** Warning + +Warns about potential circular dependencies between stores and prevents store instantiation in setup function bodies. + +Circular dependencies can cause issues in Pinia stores, especially when stores try to access each other's state during initialization. + +**❌ Incorrect:** +```js +export const useUserStore = defineStore('user', () => { + // Don't instantiate stores in setup function body + const cartStore = useCartStore() + const name = ref('John') + + return { name } +}) +``` + +**✅ Correct:** +```js +export const useUserStore = defineStore('user', () => { + const name = ref('John') + + function updateProfile() { + // Use stores in actions or computed properties + const cartStore = useCartStore() + cartStore.clear() + } + + return { name, updateProfile } +}) +``` + +### `@pinia/prefer-use-store-naming` + +**Type:** Suggestion +**Fixable:** Yes +**Recommended:** Warning + +Enforces consistent naming conventions for Pinia stores using the "useXxxStore" pattern. + +**Options:** +- `prefix` (string): Prefix for store function names (default: 'use') +- `suffix` (string): Suffix for store function names (default: 'Store') + +**❌ Incorrect:** +```js +export const userStore = defineStore('user', () => { + const name = ref('John') + return { name } +}) +``` + +**✅ Correct:** +```js +export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } +}) +``` + +### `@pinia/no-store-in-computed` + +**Type:** Problem +**Recommended:** Error + +Prevents store instantiation inside computed properties, which can cause reactivity issues. + +**❌ Incorrect:** +```js +export default { + setup() { + const userName = computed(() => { + const userStore = useUserStore() // Don't instantiate here + return userStore.name + }) + + return { userName } + } +} +``` + +**✅ Correct:** +```js +export default { + setup() { + const userStore = useUserStore() // Instantiate at top level + + const userName = computed(() => userStore.name) + + return { userName } + } +} +``` + +## Contributing + +Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository. + +## License + +MIT diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json new file mode 100644 index 0000000000..ab4e2aaa98 --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -0,0 +1,64 @@ +{ + "name": "@pinia/eslint-plugin", + "version": "1.0.0", + "description": "ESLint plugin for Pinia best practices", + "keywords": [ + "vue", + "pinia", + "eslint", + "plugin", + "linting", + "best-practices", + "store" + ], + "homepage": "https://pinia.vuejs.org", + "bugs": { + "url": "https://github.com/vuejs/pinia/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/pinia.git" + }, + "funding": "https://github.com/sponsors/posva", + "license": "MIT", + "author": { + "name": "Eduardo San Martin Morote", + "email": "posva13@gmail.com" + }, + "sideEffects": false, + "exports": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/*.js", + "dist/*.mjs", + "dist/*.d.ts" + ], + "scripts": { + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l @pinia/eslint-plugin -r 1" + }, + "devDependencies": { + "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/rule-tester": "^8.35.1", + "@typescript-eslint/utils": "^8.35.1", + "eslint": "^9.0.0", + "pinia": "workspace:*", + "tsup": "^8.5.0", + "typescript": "~5.8.3", + "vitest": "^3.2.4" + }, + "peerDependencies": { + "eslint": ">=8.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts new file mode 100644 index 0000000000..ba1faacd07 --- /dev/null +++ b/packages/eslint-plugin/src/index.ts @@ -0,0 +1,42 @@ +/** + * @fileoverview ESLint plugin for Pinia best practices + * @author Eduardo San Martin Morote + */ + +import { requireSetupStorePropertiesExport } from './rules/require-setup-store-properties-export' +import { noCircularStoreDependencies } from './rules/no-circular-store-dependencies' +import { preferUseStoreNaming } from './rules/prefer-use-store-naming' +import { noStoreInComputed } from './rules/no-store-in-computed' + +/** + * ESLint plugin for Pinia best practices and common patterns. + * + * This plugin provides rules to enforce best practices when using Pinia stores, + * including proper export patterns for setup stores, avoiding circular dependencies, + * and following naming conventions. + */ +const plugin = { + meta: { + name: '@pinia/eslint-plugin', + version: '1.0.0', + }, + rules: { + 'require-setup-store-properties-export': requireSetupStorePropertiesExport, + 'no-circular-store-dependencies': noCircularStoreDependencies, + 'prefer-use-store-naming': preferUseStoreNaming, + 'no-store-in-computed': noStoreInComputed, + }, + configs: { + recommended: { + plugins: ['@pinia'], + rules: { + '@pinia/require-setup-store-properties-export': 'error', + '@pinia/no-circular-store-dependencies': 'warn', + '@pinia/prefer-use-store-naming': 'warn', + '@pinia/no-store-in-computed': 'error', + }, + }, + }, +} + +export default plugin diff --git a/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts b/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts new file mode 100644 index 0000000000..fde87210ac --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts @@ -0,0 +1,124 @@ +/** + * @fileoverview Tests for no-circular-store-dependencies rule + */ + +import { RuleTester } from '@typescript-eslint/rule-tester' +import { noCircularStoreDependencies } from '../no-circular-store-dependencies' + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}) + +ruleTester.run('no-circular-store-dependencies', noCircularStoreDependencies, { + valid: [ + // No store dependencies + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + }, + // Store using another store in action (allowed) + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + + function updateProfile() { + const cartStore = useCartStore() + cartStore.clear() + } + + return { name, updateProfile } + }) + + export const useCartStore = defineStore('cart', () => { + const items = ref([]) + + function clear() { + items.value = [] + } + + return { items, clear } + }) + `, + }, + // Store using another store in computed (allowed) + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + + const cartSummary = computed(() => { + const cartStore = useCartStore() + return cartStore.items.length + }) + + return { name, cartSummary } + }) + `, + }, + ], + invalid: [ + // Direct store usage in setup function body + { + code: ` + export const useUserStore = defineStore('user', () => { + const cartStore = useCartStore() + const name = ref('John') + + return { name } + }) + `, + errors: [ + { + messageId: 'setupCircularDependency', + }, + ], + }, + // Store usage in variable declaration + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + const cart = useCartStore() + + return { name } + }) + `, + errors: [ + { + messageId: 'setupCircularDependency', + }, + ], + }, + // Multiple store usages in setup + { + code: ` + export const useUserStore = defineStore('user', () => { + const cartStore = useCartStore() + const orderStore = useOrderStore() + const name = ref('John') + + return { name } + }) + `, + errors: [ + { + messageId: 'setupCircularDependency', + }, + { + messageId: 'setupCircularDependency', + }, + ], + }, + ], +}) diff --git a/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts b/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts new file mode 100644 index 0000000000..17ac49b66b --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts @@ -0,0 +1,127 @@ +/** + * @fileoverview Tests for no-store-in-computed rule + */ + +import { RuleTester } from '@typescript-eslint/rule-tester' +import { noStoreInComputed } from '../no-store-in-computed' + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}) + +ruleTester.run('no-store-in-computed', noStoreInComputed, { + valid: [ + // Store instantiated outside computed + { + code: ` + export default { + setup() { + const userStore = useUserStore() + + const userName = computed(() => userStore.name) + + return { userName } + } + } + `, + }, + // No store usage in computed + { + code: ` + export default { + setup() { + const count = ref(0) + + const double = computed(() => count.value * 2) + + return { double } + } + } + `, + }, + // Store usage in regular function + { + code: ` + export default { + setup() { + function handleClick() { + const userStore = useUserStore() + userStore.updateName('New Name') + } + + return { handleClick } + } + } + `, + }, + ], + invalid: [ + // Store instantiated inside computed + { + code: ` + export default { + setup() { + const userName = computed(() => { + const userStore = useUserStore() + return userStore.name + }) + + return { userName } + } + } + `, + errors: [ + { + messageId: 'noStoreInComputed', + }, + ], + }, + // Multiple store usages in computed + { + code: ` + export default { + setup() { + const summary = computed(() => { + const userStore = useUserStore() + const cartStore = useCartStore() + return \`\${userStore.name} has \${cartStore.items.length} items\` + }) + + return { summary } + } + } + `, + errors: [ + { + messageId: 'noStoreInComputed', + }, + { + messageId: 'noStoreInComputed', + }, + ], + }, + // Store usage in arrow function computed + { + code: ` + export default { + setup() { + const userName = computed(() => useUserStore().name) + + return { userName } + } + } + `, + errors: [ + { + messageId: 'noStoreInComputed', + }, + ], + }, + ], +}) diff --git a/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts b/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts new file mode 100644 index 0000000000..f1d74d2e46 --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts @@ -0,0 +1,158 @@ +/** + * @fileoverview Tests for prefer-use-store-naming rule + */ + +import { RuleTester } from '@typescript-eslint/rule-tester' +import { preferUseStoreNaming } from '../prefer-use-store-naming' + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}) + +ruleTester.run('prefer-use-store-naming', preferUseStoreNaming, { + valid: [ + // Correct naming convention + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + }, + // Correct naming with compound name + { + code: ` + export const useShoppingCartStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) + `, + }, + // Non-store variable (should be ignored) + { + code: ` + const myVariable = someFunction() + `, + }, + ], + invalid: [ + // Missing 'use' prefix + { + code: `export const dataStore = defineStore('data', () => {})`, + errors: [ + { + messageId: 'invalidNaming', + }, + ], + output: `export const useDataStore = defineStore('data', () => {})`, + }, + // Missing 'Store' suffix + { + code: ` + export const useUser = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + errors: [ + { + messageId: 'invalidNaming', + }, + ], + output: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + }, + // Completely wrong naming + { + code: ` + export const myStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + errors: [ + { + messageId: 'invalidNaming', + }, + ], + output: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + }, + // Kebab-case store ID + { + code: ` + export const myStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) + `, + errors: [ + { + messageId: 'invalidNaming', + }, + ], + output: ` + export const useShoppingCartStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) + `, + }, + ], +}) + +// Test with custom options +ruleTester.run( + 'prefer-use-store-naming with custom options', + preferUseStoreNaming, + { + valid: [ + { + code: ` + export const createUserRepository = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + options: [{ prefix: 'create', suffix: 'Repository' }], + }, + ], + invalid: [ + { + code: ` + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + options: [{ prefix: 'create', suffix: 'Repository' }], + errors: [ + { + messageId: 'invalidNaming', + }, + ], + output: ` + export const createUserRepository = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + }, + ], + } +) diff --git a/packages/eslint-plugin/src/rules/__tests__/require-setup-store-properties-export.test.ts b/packages/eslint-plugin/src/rules/__tests__/require-setup-store-properties-export.test.ts new file mode 100644 index 0000000000..430b7c0b58 --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/require-setup-store-properties-export.test.ts @@ -0,0 +1,170 @@ +/** + * @fileoverview Tests for require-setup-store-properties-export rule + */ + +import { RuleTester } from '@typescript-eslint/rule-tester' +import { requireSetupStorePropertiesExport } from '../require-setup-store-properties-export' + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}) + +ruleTester.run( + 'require-setup-store-properties-export', + requireSetupStorePropertiesExport, + { + valid: [ + // All properties exported + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + function increment() { + count.value++ + } + + return { count, name, increment } + }) + `, + }, + // No properties defined + { + code: ` + export const useStore = defineStore('store', () => { + return {} + }) + `, + }, + // Option store (not setup store) + { + code: ` + export const useStore = defineStore('store', { + state: () => ({ count: 0 }), + actions: { + increment() { + this.count++ + } + } + }) + `, + }, + // Setup store with computed + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const double = computed(() => count.value * 2) + + return { count, double } + }) + `, + }, + ], + invalid: [ + // Missing single export + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + return { count } + }) + `, + errors: [ + { + messageId: 'missingExport', + data: { name: 'name' }, + }, + ], + output: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + return { count, name } + }) + `, + }, + // Missing multiple exports + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + function increment() { + count.value++ + } + + return { count } + }) + `, + errors: [ + { + messageId: 'missingExports', + data: { names: 'name, increment' }, + }, + ], + output: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + + function increment() { + count.value++ + } + + return { count, name, increment } + }) + `, + }, + // No return statement + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + const name = ref('test') + }) + `, + errors: [ + { + messageId: 'missingExports', + data: { names: 'count, name' }, + }, + ], + }, + // Empty return object with defined properties + { + code: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + + return {} + }) + `, + errors: [ + { + messageId: 'missingExport', + data: { name: 'count' }, + }, + ], + output: ` + export const useStore = defineStore('store', () => { + const count = ref(0) + + return { count } + }) + `, + }, + ], + } +) diff --git a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts new file mode 100644 index 0000000000..b42154fb58 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts @@ -0,0 +1,189 @@ +/** + * @fileoverview Rule to detect circular dependencies between stores + * @author Eduardo San Martin Morote + */ + +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { + isDefineStoreCall, + isSetupStore, + getSetupFunction, +} from '../utils/ast-utils' +import { isStoreUsage, getStoreNameFromUsage } from '../utils/store-utils' + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://pinia.vuejs.org/cookbook/eslint-plugin.html#${name}` +) + +/** + * Rule to detect potential circular dependencies between stores. + * + * Circular dependencies can cause issues in Pinia stores, especially when + * stores try to access each other's state during initialization. + */ +export const noCircularStoreDependencies = createRule({ + name: 'no-circular-store-dependencies', + meta: { + type: 'problem', + docs: { + description: 'disallow circular dependencies between stores', + recommended: 'warn', + }, + schema: [], + messages: { + circularDependency: + 'Potential circular dependency detected: store "{{currentStore}}" uses "{{usedStore}}"', + setupCircularDependency: + 'Avoid using other stores directly in setup function body. Use them in computed properties or actions instead.', + }, + }, + defaultOptions: [], + create(context) { + const storeUsages = new Map() // currentStore -> [usedStores] + let currentStoreName: string | null = null + + return { + CallExpression(node: TSESTree.CallExpression) { + // Track defineStore calls to identify current store + if (isDefineStoreCall(node)) { + // Get store name from variable assignment + const parent = node.parent + if ( + parent?.type === 'VariableDeclarator' && + parent.id.type === 'Identifier' + ) { + currentStoreName = parent.id.name + + // Initialize usage tracking for this store + if (!storeUsages.has(currentStoreName)) { + storeUsages.set(currentStoreName, []) + } + + // Check for store usage in setup function + if (isSetupStore(node)) { + const setupFunction = getSetupFunction(node) + if (setupFunction) { + checkSetupFunctionForStoreUsage( + setupFunction, + currentStoreName, + context + ) + } + } + } + } + + // Track store usage calls + if (isStoreUsage(node) && currentStoreName) { + const usedStoreName = getStoreNameFromUsage(node) + if (usedStoreName && usedStoreName !== currentStoreName) { + const usages = storeUsages.get(currentStoreName) || [] + if (!usages.includes(usedStoreName)) { + usages.push(usedStoreName) + storeUsages.set(currentStoreName, usages) + } + + // Check for immediate circular dependency + const usedStoreUsages = storeUsages.get(usedStoreName) || [] + if (usedStoreUsages.includes(currentStoreName)) { + context.report({ + node, + messageId: 'circularDependency', + data: { + currentStore: currentStoreName, + usedStore: usedStoreName, + }, + }) + } + } + } + }, + + 'Program:exit'() { + // Check for indirect circular dependencies + checkIndirectCircularDependencies(storeUsages, context) + }, + } + }, +}) + +/** + * Checks setup function for direct store usage in the function body + */ +function checkSetupFunctionForStoreUsage( + setupFunction: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + currentStoreName: string, + context: any +) { + if (setupFunction.body.type !== 'BlockStatement') { + return + } + + // Look for store usage calls in the top level of setup function + for (const statement of setupFunction.body.body) { + if (statement.type === 'VariableDeclaration') { + for (const declarator of statement.declarations) { + if ( + declarator.init?.type === 'CallExpression' && + isStoreUsage(declarator.init) + ) { + context.report({ + node: declarator.init, + messageId: 'setupCircularDependency', + }) + } + } + } else if ( + statement.type === 'ExpressionStatement' && + statement.expression.type === 'CallExpression' && + isStoreUsage(statement.expression) + ) { + context.report({ + node: statement.expression, + messageId: 'setupCircularDependency', + }) + } + } +} + +/** + * Checks for indirect circular dependencies (A -> B -> C -> A) + */ +function checkIndirectCircularDependencies( + storeUsages: Map, + context: any +) { + const visited = new Set() + const recursionStack = new Set() + + function hasCycle(store: string, path: string[] = []): boolean { + if (recursionStack.has(store)) { + // Found a cycle + return true + } + + if (visited.has(store)) { + return false + } + + visited.add(store) + recursionStack.add(store) + + const dependencies = storeUsages.get(store) || [] + for (const dependency of dependencies) { + if (hasCycle(dependency, [...path, store])) { + return true + } + } + + recursionStack.delete(store) + return false + } + + // Check each store for cycles + for (const store of storeUsages.keys()) { + if (!visited.has(store)) { + hasCycle(store) + } + } +} diff --git a/packages/eslint-plugin/src/rules/no-store-in-computed.ts b/packages/eslint-plugin/src/rules/no-store-in-computed.ts new file mode 100644 index 0000000000..e6ec8c0508 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-store-in-computed.ts @@ -0,0 +1,111 @@ +/** + * @fileoverview Rule to prevent store instantiation in computed properties + * @author Eduardo San Martin Morote + */ + +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { isStoreUsage } from '../utils/store-utils' + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://pinia.vuejs.org/cookbook/eslint-plugin.html#${name}` +) + +/** + * Rule to prevent store instantiation inside computed properties. + * + * Stores should be instantiated at the top level of components or composables, + * not inside computed properties, as this can cause reactivity issues. + */ +export const noStoreInComputed = createRule({ + name: 'no-store-in-computed', + meta: { + type: 'problem', + docs: { + description: 'disallow store instantiation in computed properties', + recommended: 'error', + }, + schema: [], + messages: { + noStoreInComputed: + 'Avoid instantiating stores inside computed properties. Move store instantiation to the top level.', + }, + }, + defaultOptions: [], + create(context) { + let insideComputed = false + + return { + CallExpression(node: TSESTree.CallExpression) { + // Track when we enter a computed() call + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'computed' && + node.arguments.length > 0 + ) { + insideComputed = true + + // Check the computed function for store usage + const computedFn = node.arguments[0] + if ( + computedFn.type === 'FunctionExpression' || + computedFn.type === 'ArrowFunctionExpression' + ) { + checkFunctionForStoreUsage(computedFn, context) + } + + insideComputed = false + } + + // Check for store usage inside computed + if (insideComputed && isStoreUsage(node)) { + context.report({ + node, + messageId: 'noStoreInComputed', + }) + } + }, + } + }, +}) + +/** + * Recursively checks a function for store usage + */ +function checkFunctionForStoreUsage( + fn: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + context: any +) { + const visited = new Set() + + function visitNode(node: TSESTree.Node) { + if (visited.has(node)) { + return + } + visited.add(node) + + if (node.type === 'CallExpression' && isStoreUsage(node)) { + context.report({ + node, + messageId: 'noStoreInComputed', + }) + } + + // Recursively visit child nodes + for (const key in node) { + const child = (node as any)[key] + if (child && typeof child === 'object' && child !== node.parent) { + if (Array.isArray(child)) { + child.forEach(visitNode) + } else if (child.type) { + visitNode(child) + } + } + } + } + + if (fn.body.type === 'BlockStatement') { + fn.body.body.forEach(visitNode) + } else { + visitNode(fn.body) + } +} diff --git a/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts new file mode 100644 index 0000000000..501603596e --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts @@ -0,0 +1,111 @@ +/** + * @fileoverview Rule to enforce consistent store naming conventions + * @author Eduardo San Martin Morote + */ + +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { isDefineStoreCall } from '../utils/ast-utils' + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://pinia.vuejs.org/cookbook/eslint-plugin.html#${name}` +) + +/** + * Rule to enforce consistent naming conventions for Pinia stores. + * + * Encourages using the "useXxxStore" pattern for store functions, + * which is a common convention in the Vue ecosystem. + */ +export const preferUseStoreNaming = createRule({ + name: 'prefer-use-store-naming', + meta: { + type: 'suggestion', + docs: { + description: 'enforce consistent store naming conventions', + recommended: 'warn', + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + prefix: { + type: 'string', + default: 'use', + }, + suffix: { + type: 'string', + default: 'Store', + }, + }, + additionalProperties: false, + }, + ], + messages: { + invalidNaming: + 'Store function should follow the naming pattern "{{prefix}}{{name}}{{suffix}}"', + }, + }, + defaultOptions: [{ prefix: 'use', suffix: 'Store' }], + create(context, [options = {}]) { + const { prefix = 'use', suffix = 'Store' } = options + + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + // Check if this is a store definition + if ( + node.init?.type === 'CallExpression' && + isDefineStoreCall(node.init) && + node.id.type === 'Identifier' + ) { + const storeName = node.id.name + + // Check naming convention + if (!storeName.startsWith(prefix) || !storeName.endsWith(suffix)) { + // Extract the core name from store ID if available + let suggestedName = storeName + if ( + node.init.arguments.length > 0 && + node.init.arguments[0].type === 'Literal' && + typeof node.init.arguments[0].value === 'string' + ) { + const storeId = node.init.arguments[0].value + // Convert kebab-case or snake_case to PascalCase + const coreName = storeId + .split(/[-_]/) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + suggestedName = `${prefix}${coreName}${suffix}` + } else { + // If no store ID, try to fix the current name + let baseName = storeName + if (storeName.startsWith(prefix)) { + baseName = storeName.slice(prefix.length) + } + if (storeName.endsWith(suffix)) { + baseName = baseName.slice(0, -suffix.length) + } + if (!baseName) { + baseName = 'Store' + } + suggestedName = `${prefix}${baseName.charAt(0).toUpperCase() + baseName.slice(1)}${suffix}` + } + + context.report({ + node: node.id, + messageId: 'invalidNaming', + data: { + prefix, + name: '{{Name}}', + suffix, + }, + fix(fixer) { + return fixer.replaceText(node.id, suggestedName) + }, + }) + } + } + }, + } + }, +}) diff --git a/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts new file mode 100644 index 0000000000..4767b73e92 --- /dev/null +++ b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts @@ -0,0 +1,132 @@ +/** + * @fileoverview Rule to require all setup store properties to be exported + * @author Eduardo San Martin Morote + */ + +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { + isDefineStoreCall, + isSetupStore, + getSetupFunction, + extractDeclarations, + extractReturnProperties, + findReturnStatement, +} from '../utils/ast-utils' + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://pinia.vuejs.org/cookbook/eslint-plugin.html#${name}` +) + +/** + * Rule to ensure all variables and functions in setup stores are properly exported. + * + * 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. + */ +export const requireSetupStorePropertiesExport = createRule({ + name: 'require-setup-store-properties-export', + meta: { + type: 'problem', + docs: { + description: 'require all setup store properties to be exported', + recommended: 'error', + }, + fixable: 'code', + schema: [], + messages: { + missingExport: + 'Property "{{name}}" is defined but not exported from setup store', + missingExports: + 'Properties {{names}} are defined but not exported from setup store', + }, + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node: TSESTree.CallExpression) { + // Only check defineStore calls + if (!isDefineStoreCall(node) || !isSetupStore(node)) { + return + } + + const setupFunction = getSetupFunction(node) + if (!setupFunction || setupFunction.body.type !== 'BlockStatement') { + return + } + + // Extract all declared variables and functions + const { variables, functions } = extractDeclarations(setupFunction.body) + const allDeclared = [...variables, ...functions] + + // Find the return statement + const returnStatement = findReturnStatement(setupFunction.body) + if (!returnStatement) { + // If there's no return statement, all declared items are missing + if (allDeclared.length > 0) { + context.report({ + node: setupFunction, + messageId: + allDeclared.length === 1 ? 'missingExport' : 'missingExports', + data: { + name: allDeclared[0], + names: allDeclared.join(', '), + }, + }) + } + return + } + + // Extract exported properties + const exportedProperties = extractReturnProperties(returnStatement) + + // Find missing exports + const missingExports = allDeclared.filter( + (name) => !exportedProperties.includes(name) + ) + + if (missingExports.length > 0) { + context.report({ + node: returnStatement, + messageId: + missingExports.length === 1 ? 'missingExport' : 'missingExports', + data: { + name: missingExports[0], + names: missingExports.join(', '), + }, + fix(fixer) { + // Auto-fix: add missing properties to return object + if ( + !returnStatement.argument || + returnStatement.argument.type !== 'ObjectExpression' + ) { + return null + } + + const objectExpression = returnStatement.argument + const existingProperties = objectExpression.properties + + // Create new properties for missing exports + const newProperties = missingExports.map((name) => `${name}`) + + if (existingProperties.length === 0) { + // Empty object, add all properties + return fixer.replaceText( + objectExpression, + `{ ${newProperties.join(', ')} }` + ) + } else { + // Add to existing properties + const lastProperty = + existingProperties[existingProperties.length - 1] + return fixer.insertTextAfter( + lastProperty, + `, ${newProperties.join(', ')}` + ) + } + }, + }) + } + }, + } + }, +}) diff --git a/packages/eslint-plugin/src/types.ts b/packages/eslint-plugin/src/types.ts new file mode 100644 index 0000000000..ad7aa45f9e --- /dev/null +++ b/packages/eslint-plugin/src/types.ts @@ -0,0 +1,99 @@ +/** + * @fileoverview Type definitions for Pinia ESLint plugin + */ + +import type { TSESTree } from '@typescript-eslint/utils' + +/** + * Configuration options for the prefer-use-store-naming rule + */ +export interface PreferUseStoreNamingOptions { + /** Prefix for store function names (default: 'use') */ + prefix?: string + /** Suffix for store function names (default: 'Store') */ + suffix?: string +} + +/** + * Information about a Pinia store definition + */ +export interface StoreDefinitionInfo { + /** The store ID (first argument to defineStore) */ + id: string | null + /** Whether this is a setup store (function as second argument) */ + isSetupStore: boolean + /** The setup function if this is a setup store */ + setupFunction: + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | null + /** The variable name the store is assigned to */ + variableName: string | null + /** The AST node of the defineStore call */ + node: TSESTree.CallExpression +} + +/** + * Information about declared variables and functions in a setup store + */ +export interface SetupStoreDeclarations { + /** Names of declared variables */ + variables: string[] + /** Names of declared functions */ + functions: string[] + /** All declared names combined */ + all: string[] +} + +/** + * Information about exported properties from a setup store + */ +export interface SetupStoreExports { + /** Names of exported properties */ + properties: string[] + /** The return statement node */ + returnStatement: TSESTree.ReturnStatement | null + /** Whether the return object uses spread syntax */ + hasSpread: boolean +} + +/** + * Store dependency information for circular dependency detection + */ +export interface StoreDependency { + /** Name of the store that has the dependency */ + storeName: string + /** Names of stores this store depends on */ + dependencies: string[] + /** AST nodes where dependencies are used */ + usageNodes: TSESTree.CallExpression[] +} + +/** + * Plugin configuration options + */ +export interface PluginConfig { + /** Rules configuration */ + rules?: { + 'require-setup-store-properties-export'?: 'error' | 'warn' | 'off' + 'no-circular-store-dependencies'?: 'error' | 'warn' | 'off' + 'prefer-use-store-naming'?: + | 'error' + | 'warn' + | 'off' + | [string, PreferUseStoreNamingOptions] + 'no-store-in-computed'?: 'error' | 'warn' | 'off' + } +} + +/** + * ESLint rule context with Pinia-specific utilities + */ +export interface PiniaRuleContext { + /** Report an error or warning */ + report: (descriptor: any) => void + /** Get the source code */ + getSourceCode: () => any + /** Get rule options */ + options: any[] +} diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts new file mode 100644 index 0000000000..07acf9aac6 --- /dev/null +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -0,0 +1,116 @@ +/** + * @fileoverview AST utilities for Pinia ESLint plugin + */ + +import type { TSESTree } from '@typescript-eslint/utils' + +/** + * Checks if a node is a call expression to `defineStore` + */ +export function isDefineStoreCall( + node: TSESTree.Node +): node is TSESTree.CallExpression { + return ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'defineStore' + ) +} + +/** + * Checks if a call expression is a setup store (has a function as second argument) + */ +export function isSetupStore(node: TSESTree.CallExpression): boolean { + return ( + node.arguments.length >= 2 && + (node.arguments[1].type === 'FunctionExpression' || + node.arguments[1].type === 'ArrowFunctionExpression') + ) +} + +/** + * Gets the setup function from a defineStore call + */ +export function getSetupFunction( + node: TSESTree.CallExpression +): TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | null { + if (!isSetupStore(node)) { + return null + } + + const setupArg = node.arguments[1] + if ( + setupArg.type === 'FunctionExpression' || + setupArg.type === 'ArrowFunctionExpression' + ) { + return setupArg + } + + return null +} + +/** + * Extracts variable and function declarations from a function body + */ +export function extractDeclarations(body: TSESTree.BlockStatement): { + variables: string[] + functions: string[] +} { + const variables: string[] = [] + const functions: string[] = [] + + for (const statement of body.body) { + if (statement.type === 'VariableDeclaration') { + for (const declarator of statement.declarations) { + if (declarator.id.type === 'Identifier') { + variables.push(declarator.id.name) + } + } + } else if (statement.type === 'FunctionDeclaration' && statement.id) { + functions.push(statement.id.name) + } + } + + return { variables, functions } +} + +/** + * Extracts properties from a return statement object + */ +export function extractReturnProperties( + returnStatement: TSESTree.ReturnStatement +): string[] { + if ( + !returnStatement.argument || + returnStatement.argument.type !== 'ObjectExpression' + ) { + return [] + } + + const properties: string[] = [] + + for (const prop of returnStatement.argument.properties) { + if (prop.type === 'Property' && prop.key.type === 'Identifier') { + properties.push(prop.key.name) + } else if (prop.type === 'SpreadElement') { + // Handle spread elements - we can't easily determine what's being spread + // so we'll be more lenient in this case + } + } + + return properties +} + +/** + * Finds the return statement in a function body + */ +export function findReturnStatement( + body: TSESTree.BlockStatement +): TSESTree.ReturnStatement | null { + for (const statement of body.body) { + if (statement.type === 'ReturnStatement') { + return statement + } + } + return null +} diff --git a/packages/eslint-plugin/src/utils/store-utils.ts b/packages/eslint-plugin/src/utils/store-utils.ts new file mode 100644 index 0000000000..b93b76461f --- /dev/null +++ b/packages/eslint-plugin/src/utils/store-utils.ts @@ -0,0 +1,95 @@ +/** + * @fileoverview Store-specific utilities for Pinia ESLint plugin + */ + +import type { TSESTree } from '@typescript-eslint/utils' +import { isDefineStoreCall, isSetupStore, getSetupFunction } from './ast-utils' + +/** + * Information about a Pinia store definition + */ +export interface StoreInfo { + /** The store ID (first argument to defineStore) */ + id: string | null + /** Whether this is a setup store */ + isSetupStore: boolean + /** The setup function if this is a setup store */ + setupFunction: + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | null + /** The variable name the store is assigned to */ + variableName: string | null +} + +/** + * Analyzes a defineStore call and extracts store information + */ +export function analyzeStoreDefinition( + node: TSESTree.CallExpression +): StoreInfo | null { + if (!isDefineStoreCall(node)) { + return null + } + + // Extract store ID (first argument) + let id: string | null = null + if ( + node.arguments.length > 0 && + node.arguments[0].type === 'Literal' && + typeof node.arguments[0].value === 'string' + ) { + id = node.arguments[0].value + } + + // Check if it's a setup store + const setupStore = isSetupStore(node) + const setupFunction = setupStore ? getSetupFunction(node) : null + + return { + id, + isSetupStore: setupStore, + setupFunction, + variableName: null, // Will be filled by the caller if needed + } +} + +/** + * Checks if a node represents a store usage (calling a store function) + */ +export function isStoreUsage(node: TSESTree.CallExpression): boolean { + // Look for patterns like useStore() or useMyStore() + return ( + node.callee.type === 'Identifier' && + node.callee.name.startsWith('use') && + node.callee.name.endsWith('Store') + ) +} + +/** + * Extracts the store name from a store usage call + */ +export function getStoreNameFromUsage( + node: TSESTree.CallExpression +): string | null { + if (!isStoreUsage(node)) { + return null + } + + if (node.callee.type === 'Identifier') { + return node.callee.name + } + + return null +} + +/** + * Checks if a variable declaration is a store definition + */ +export function isStoreDefinition(node: TSESTree.VariableDeclarator): boolean { + return ( + node.init !== null && + node.init.type === 'CallExpression' && + isDefineStoreCall(node.init) + ) +} diff --git a/packages/eslint-plugin/test_output.txt b/packages/eslint-plugin/test_output.txt new file mode 100644 index 0000000000..f5bcab74b2 --- /dev/null +++ b/packages/eslint-plugin/test_output.txt @@ -0,0 +1,190 @@ + +> @pinia/eslint-plugin@1.0.0 test D:\pych_projects\pinia\packages\eslint-plugin +> vitest run + + + RUN  v3.2.4 D:/pych_projects/pinia/packages/eslint-plugin + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > valid >  + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: useUserStore prefix: use suffix: Store +Checking: useUserStore startsWithPrefix: true endsWithSuffix: true + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > valid >  + export const useShoppingCartStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) + +Found store definition: useShoppingCartStore prefix: use suffix: Store +Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  + export const userStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: userStore prefix: use suffix: Store +Checking: userStore startsWithPrefix: true endsWithSuffix: true + + тЬУ src/rules/__tests__/no-store-in-computed.test.ts (6 tests) 57ms +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  + export const useUser = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: useUser prefix: use suffix: Store +Checking: useUser startsWithPrefix: true endsWithSuffix: false +Naming violation detected for: useUser +Found store definition: useUserStore prefix: use suffix: Store +Checking: useUserStore startsWithPrefix: true endsWithSuffix: true +Found store definition: useUserStore prefix: use suffix: Store +Checking: useUserStore startsWithPrefix: true endsWithSuffix: true + + тЬУ src/rules/__tests__/no-circular-store-dependencies.test.ts (6 tests) 58ms +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  + export const myStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: myStore prefix: use suffix: Store +Checking: myStore startsWithPrefix: false endsWithSuffix: true +Naming violation detected for: myStore +Found store definition: useUserStore prefix: use suffix: Store +Checking: useUserStore startsWithPrefix: true endsWithSuffix: true +Found store definition: useUserStore prefix: use suffix: Store +Checking: useUserStore startsWithPrefix: true endsWithSuffix: true + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  + export const myStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) + +Found store definition: myStore prefix: use suffix: Store +Checking: myStore startsWithPrefix: false endsWithSuffix: true +Naming violation detected for: myStore +Found store definition: useShoppingCartStore prefix: use suffix: Store +Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true +Found store definition: useShoppingCartStore prefix: use suffix: Store +Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming with custom options > valid >  + export const createUserRepository = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: createUserRepository prefix: create suffix: Repository +Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true + +stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming with custom options > invalid >  + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +Found store definition: useUserStore prefix: create suffix: Repository +Checking: useUserStore startsWithPrefix: false endsWithSuffix: false +Naming violation detected for: useUserStore +Found store definition: createUserRepository prefix: create suffix: Repository +Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true +Found store definition: createUserRepository prefix: create suffix: Repository +Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true + + тЬУ src/rules/__tests__/require-setup-store-properties-export.test.ts (8 tests) 72ms + тЭп src/rules/__tests__/prefer-use-store-naming.test.ts (9 tests | 1 failed) 78ms + тЬУ prefer-use-store-naming > valid >  + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  33ms + тЬУ prefer-use-store-naming > valid >  + export const useShoppingCartStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) +  5ms + тЬУ prefer-use-store-naming > valid >  + const myVariable = someFunction() +  2ms + ├Ч prefer-use-store-naming > invalid >  + export const userStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  7ms + тЖТ Should have 1 error but had 0: [] + +0 !== 1 + + тЬУ prefer-use-store-naming > invalid >  + export const useUser = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  11ms + тЬУ prefer-use-store-naming > invalid >  + export const myStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  8ms + тЬУ prefer-use-store-naming > invalid >  + export const myStore = defineStore('shopping-cart', () => { + const items = ref([]) + return { items } + }) +  4ms + тЬУ prefer-use-store-naming with custom options > valid >  + export const createUserRepository = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  2ms + тЬУ prefer-use-store-naming with custom options > invalid >  + export const useUserStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) +  4ms + +тОптОптОптОптОптОптОп Failed Tests 1 тОптОптОптОптОптОптОп + + FAIL src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid > + export const userStore = defineStore('user', () => { + const name = ref('John') + return { name } + }) + +AssertionError: Should have 1 error but had 0: [] + +0 !== 1 + + +- Expected ++ Received + +- 1 ++ 0 + + тЭп RuleTester.#testInvalidTemplate ../../node_modules/.pnpm/@typescript-eslint+rule-tes_4d91085b7cef78db45b5c8037c2a1f8c/node_modules/@typescript-eslint/rule-tester/dist/RuleTester.js:706:35 + тЭп ../../node_modules/.pnpm/@typescript-eslint+rule-tes_4d91085b7cef78db45b5c8037c2a1f8c/node_modules/@typescript-eslint/rule-tester/dist/RuleTester.js:448:58 + +тОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОп[1/1]тОп + + + Test Files  1 failed | 3 passed (4) + Tests  1 failed | 28 passed (29) + Start at  09:21:23 + Duration  1.08s (transform 110ms, setup 0ms, collect 2.47s, tests 266ms, environment 1ms, prepare 607ms) + +тАЙELIFECYCLEтАЙ Test failed. See above for more details. diff --git a/packages/eslint-plugin/tsconfig.build.json b/packages/eslint-plugin/tsconfig.build.json new file mode 100644 index 0000000000..6be56f2da0 --- /dev/null +++ b/packages/eslint-plugin/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/eslint-plugin/tsup.config.ts b/packages/eslint-plugin/tsup.config.ts new file mode 100644 index 0000000000..22abee19ef --- /dev/null +++ b/packages/eslint-plugin/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + external: ['eslint'], + target: 'node14', + splitting: false, + sourcemap: true, +}) diff --git a/packages/eslint-plugin/vitest.config.ts b/packages/eslint-plugin/vitest.config.ts new file mode 100644 index 0000000000..1e0c9224f9 --- /dev/null +++ b/packages/eslint-plugin/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['src/**/*.{test,spec}.ts'], + environment: 'node', + globals: true, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d79cf180e..15b9d4e86f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,33 @@ importers: specifier: ^0.3.3 version: 0.3.3(@vue/composition-api@1.7.2(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)) + packages/eslint-plugin: + devDependencies: + '@typescript-eslint/parser': + specifier: ^8.35.1 + version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/rule-tester': + specifier: ^8.35.1 + version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': + specifier: ^8.35.1 + version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + eslint: + specifier: ^9.0.0 + version: 9.35.0(jiti@2.4.2) + pinia: + specifier: workspace:* + version: link:../pinia + tsup: + specifier: ^8.5.0 + version: 8.5.0(@microsoft/api-extractor@7.49.2(@types/node@24.0.8))(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0) + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.0.8)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) + packages/nuxt: dependencies: '@nuxt/kit': @@ -153,7 +180,7 @@ importers: version: 3.19.1(@types/node@24.0.8)(@vitest/ui@3.2.4)(@vue/test-utils@2.4.6)(happy-dom@16.8.1)(jiti@2.4.2)(magicast@0.3.5)(terser@5.36.0)(typescript@5.8.3)(vitest@3.2.4)(yaml@2.8.0) nuxt: specifier: ^3.17.5 - version: 3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.8)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) + version: 3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.8)(db0@0.3.2)(encoding@0.1.13)(eslint@9.35.0(jiti@2.4.2))(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) pinia: specifier: workspace:^ version: link:../pinia @@ -829,12 +856,66 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.35.0': + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} '@gerrit0/mini-shiki@1.27.2': resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} @@ -1510,6 +1591,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -1565,32 +1649,82 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/parser@8.43.0': + resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.35.1': resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/project-service@8.43.0': + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/rule-tester@8.43.0': + resolution: {integrity: sha512-DZNnTOjVz9fkZl5Az6h5r0FLfmnw2N2jHLHUluTwKZSs6wZBpIseRBSGmSIoTnye2dmOxagEzFfFQ/OoluIHJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/scope-manager@8.43.0': + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.35.1': resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/tsconfig-utils@8.43.0': + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.35.1': resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.43.0': + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.35.1': resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/typescript-estree@8.43.0': + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.43.0': + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.35.1': resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.43.0': + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -1889,6 +2023,11 @@ packages: peerDependencies: acorn: ^8 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1917,6 +2056,9 @@ packages: ajv: optional: true + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} @@ -2103,6 +2245,10 @@ packages: callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -2128,6 +2274,10 @@ packages: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -2492,6 +2642,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2723,6 +2876,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -2732,15 +2889,45 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.35.0: + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2807,6 +2994,12 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-npm-meta@0.4.4: resolution: {integrity: sha512-cq8EVW3jpX1U3dO1AYanz2BJ6n9ITQgCwE1xjNwI5jO2a9erE369OZNO8Wt/Wbw8YHhCD/dimH9BxRsY+6DinA==} @@ -2838,6 +3031,10 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + file-saver@2.0.5: resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} @@ -2868,6 +3065,10 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + find-up@7.0.0: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} @@ -2875,6 +3076,10 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -2994,6 +3199,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -3011,6 +3220,10 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + globby@14.1.0: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} @@ -3123,6 +3336,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -3133,6 +3350,10 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -3361,20 +3582,33 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -3401,6 +3635,9 @@ packages: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -3438,6 +3675,10 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -3484,6 +3725,10 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + locate-path@7.2.0: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3509,6 +3754,9 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -3679,6 +3927,9 @@ packages: minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -3783,6 +4034,9 @@ packages: nanotar@0.2.0: resolution: {integrity: sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -3951,6 +4205,10 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + oxc-parser@0.72.3: resolution: {integrity: sha512-JYQeJKDcUTTZ/uTdJ+fZBGFjAjkLD1h0p3Tf44ZYXRcoMk+57d81paNPFAAwzrzzqhZmkGvKKXDxwyhJXYZlpg==} engines: {node: '>=14.0.0'} @@ -3967,6 +4225,10 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3979,6 +4241,10 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-locate@6.0.0: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4016,6 +4282,10 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-gitignore@2.0.0: resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} engines: {node: '>=14'} @@ -4358,6 +4628,10 @@ packages: engines: {node: '>=18'} hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} @@ -4521,6 +4795,10 @@ packages: require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5058,6 +5336,10 @@ packages: typescript: optional: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -5675,6 +5957,10 @@ packages: resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -5751,6 +6037,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.1: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} @@ -6300,6 +6590,50 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0(jiti@2.4.2))': + dependencies: + eslint: 9.35.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.35.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + '@fastify/busboy@3.1.1': {} '@gerrit0/mini-shiki@1.27.2': @@ -6308,6 +6642,17 @@ snapshots: '@shikijs/types': 1.29.2 '@shikijs/vscode-textmate': 10.0.1 + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@hutson/parse-repository-url@3.0.2': {} '@iconify-json/simple-icons@1.2.22': @@ -6730,7 +7075,7 @@ snapshots: - typescript - yaml - '@nuxt/vite-builder@3.17.5(@types/node@24.0.8)(magicast@0.3.5)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vue-tsc@2.2.10(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3))(yaml@2.8.0)': + '@nuxt/vite-builder@3.17.5(@types/node@24.0.8)(eslint@9.35.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vue-tsc@2.2.10(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3))(yaml@2.8.0)': dependencies: '@nuxt/kit': 3.17.5(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.44.1) @@ -6763,7 +7108,7 @@ snapshots: unplugin: 2.3.5 vite: 6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) vite-node: 3.2.4(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) - vite-plugin-checker: 0.9.3(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3)) + vite-plugin-checker: 0.9.3(eslint@9.35.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3)) vue: 3.5.17(typescript@5.8.3) vue-bundle-renderer: 2.1.1 transitivePeerDependencies: @@ -7173,6 +7518,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/linkify-it@5.0.0': {} '@types/lodash.kebabcase@4.1.9': @@ -7225,6 +7572,18 @@ snapshots: '@types/node': 24.0.8 optional: true + '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + eslint: 9.35.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) @@ -7234,12 +7593,46 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.43.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) + '@typescript-eslint/types': 8.43.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/rule-tester@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + ajv: 6.12.6 + eslint: 9.35.0(jiti@2.4.2) + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/scope-manager@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + '@typescript-eslint/types@8.35.1': {} + '@typescript-eslint/types@8.43.0': {} + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': dependencies: '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) @@ -7256,11 +7649,43 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.43.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + eslint: 9.35.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.35.1': dependencies: '@typescript-eslint/types': 8.35.1 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.2.0': {} '@unhead/vue@2.0.11(vue@3.5.17(typescript@5.8.3))': @@ -7379,7 +7804,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.0.8)(@vitest/ui@3.2.4)(happy-dom@16.8.1)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) + vitest: 3.2.4(@types/node@24.0.8)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -7678,6 +8103,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} add-stream@1.0.0: {} @@ -7692,6 +8121,13 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.12.0: dependencies: fast-deep-equal: 3.1.3 @@ -7903,6 +8339,8 @@ snapshots: callsite@1.0.0: {} + callsites@3.1.0: {} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -7932,6 +8370,11 @@ snapshots: loupe: 3.1.4 pathval: 2.0.1 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.4.1: {} change-case@5.4.4: @@ -8311,6 +8754,8 @@ snapshots: deep-eql@5.0.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.0: {} @@ -8561,6 +9006,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} escodegen@2.1.0: @@ -8571,10 +9018,73 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.35.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.35.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -8655,6 +9165,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-npm-meta@0.4.4: {} fastq@1.17.1: @@ -8682,6 +9196,10 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + file-saver@2.0.5: {} file-uri-to-path@1.0.0: {} @@ -8709,6 +9227,11 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + find-up@7.0.0: dependencies: locate-path: 7.2.0 @@ -8721,6 +9244,11 @@ snapshots: mlly: 1.7.4 rollup: 4.44.1 + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + flatted@3.3.3: {} fn.name@1.1.0: {} @@ -8854,6 +9382,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@10.4.5: dependencies: foreground-child: 3.2.1 @@ -8878,6 +9410,8 @@ snapshots: globals@11.12.0: {} + globals@14.0.0: {} + globby@14.1.0: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -9008,12 +9542,19 @@ snapshots: ieee754@1.2.1: {} + ignore@5.3.2: {} + ignore@7.0.5: {} image-meta@0.2.1: {} immediate@3.0.6: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-lazy@4.0.0: {} impound@1.0.0: @@ -9204,14 +9745,24 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} json5@2.2.3: {} @@ -9235,6 +9786,10 @@ snapshots: jwt-decode@4.0.0: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kind-of@6.0.3: {} kleur@3.0.3: {} @@ -9264,6 +9819,11 @@ snapshots: dependencies: readable-stream: 2.3.8 + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -9345,6 +9905,10 @@ snapshots: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + locate-path@7.2.0: dependencies: p-locate: 6.0.0 @@ -9363,6 +9927,8 @@ snapshots: lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} lodash.uniq@4.5.0: {} @@ -9543,6 +10109,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -9632,6 +10202,8 @@ snapshots: nanotar@0.2.0: {} + natural-compare@1.4.0: {} + neo-async@2.6.2: {} netlify@13.3.5: @@ -9822,7 +10394,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.8)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0): + nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.8)(db0@0.3.2)(encoding@0.1.13)(eslint@9.35.0(jiti@2.4.2))(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0): dependencies: '@nuxt/cli': 3.25.1(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -9830,7 +10402,7 @@ snapshots: '@nuxt/kit': 3.17.5(magicast@0.3.5) '@nuxt/schema': 3.17.5 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.17.5(@types/node@24.0.8)(magicast@0.3.5)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vue-tsc@2.2.10(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3))(yaml@2.8.0) + '@nuxt/vite-builder': 3.17.5(@types/node@24.0.8)(eslint@9.35.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.44.1)(terser@5.36.0)(typescript@5.8.3)(vue-tsc@2.2.10(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3))(yaml@2.8.0) '@unhead/vue': 2.0.11(vue@3.5.17(typescript@5.8.3)) '@vue/shared': 3.5.17 c12: 3.0.4(magicast@0.3.5) @@ -10002,6 +10574,15 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + oxc-parser@0.72.3: dependencies: '@oxc-project/types': 0.72.3 @@ -10033,6 +10614,10 @@ snapshots: dependencies: p-try: 2.2.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-limit@4.0.0: dependencies: yocto-queue: 1.2.1 @@ -10045,6 +10630,10 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-locate@6.0.0: dependencies: p-limit: 4.0.0 @@ -10069,6 +10658,10 @@ snapshots: pako@1.0.11: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-gitignore@2.0.0: {} parse-json@4.0.0: @@ -10380,6 +10973,8 @@ snapshots: transitivePeerDependencies: - supports-color + prelude-ls@1.2.1: {} + prettier@3.6.2: {} pretty-bytes@6.1.1: {} @@ -10539,6 +11134,8 @@ snapshots: require-package-name@2.0.1: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve@1.22.10: @@ -11098,6 +11695,10 @@ snapshots: - tsx - yaml + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@0.18.1: {} type-fest@0.6.0: {} @@ -11394,7 +11995,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.9.3(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3)): + vite-plugin-checker@0.9.3(eslint@9.35.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -11407,6 +12008,8 @@ snapshots: vite: 6.3.5(@types/node@24.0.8)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) vscode-uri: 3.1.0 optionalDependencies: + eslint: 9.35.0(jiti@2.4.2) + optionator: 0.9.4 typescript: 5.8.3 vue-tsc: 2.2.10(typescript@5.8.3) @@ -11652,6 +12255,7 @@ snapshots: - terser - tsx - yaml + optional: true vitest@3.2.4(@types/node@24.0.8)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0): dependencies: @@ -11811,6 +12415,8 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.9.0 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@7.0.0: @@ -11881,6 +12487,8 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yocto-queue@0.1.0: {} + yocto-queue@1.2.1: {} yoctocolors@2.1.1: {} From 0259ebf79f3aba5d1ef03df1c649b4fe70d0550c Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Mon, 15 Sep 2025 10:07:00 +0300 Subject: [PATCH 2/7] fix(eslint-plugin): address CodeRabbit review comments - Fix AST parsing to handle destructuring patterns and aliasing - Add extractReturnIdentifiers function for better return property detection - Handle spread elements to avoid false positives - Improve type safety by removing any types - Support object-form defineStore calls with id property - Support qualified/namespaced store calls - Fix message interpolation in prefer-use-store-naming rule - Change autofixes to suggestions for safer refactoring - Update documentation to align with rule behavior --- packages/eslint-plugin/README.md | 2 +- packages/eslint-plugin/package.json | 3 + packages/eslint-plugin/src/index.ts | 15 +- .../__tests__/prefer-use-store-naming.test.ts | 51 +++-- .../rules/no-circular-store-dependencies.ts | 104 ++++++---- .../src/rules/no-store-in-computed.ts | 79 +++++--- .../src/rules/prefer-use-store-naming.ts | 29 ++- .../require-setup-store-properties-export.ts | 14 +- packages/eslint-plugin/src/utils/ast-utils.ts | 129 +++++++++++- packages/eslint-plugin/test_output.txt | 190 ------------------ packages/eslint-plugin/tsup.config.ts | 7 +- 11 files changed, 318 insertions(+), 305 deletions(-) delete mode 100644 packages/eslint-plugin/test_output.txt diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 7c4035f929..933b54dfab 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -128,7 +128,7 @@ export const useUserStore = defineStore('user', () => { const name = ref('John') function updateProfile() { - // Use stores in actions or computed properties + // Use stores in actions const cartStore = useCartStore() cartStore.clear() } diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ab4e2aaa98..d24a46f000 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -55,6 +55,9 @@ "typescript": "~5.8.3", "vitest": "^3.2.4" }, + "engines": { + "node": ">=18.18.0" + }, "peerDependencies": { "eslint": ">=8.0.0" }, diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index ba1faacd07..34516b45ea 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -7,6 +7,7 @@ import { requireSetupStorePropertiesExport } from './rules/require-setup-store-p import { noCircularStoreDependencies } from './rules/no-circular-store-dependencies' import { preferUseStoreNaming } from './rules/prefer-use-store-naming' import { noStoreInComputed } from './rules/no-store-in-computed' +import packageJson from '../package.json' /** * ESLint plugin for Pinia best practices and common patterns. @@ -18,7 +19,7 @@ import { noStoreInComputed } from './rules/no-store-in-computed' const plugin = { meta: { name: '@pinia/eslint-plugin', - version: '1.0.0', + version: packageJson.version, }, rules: { 'require-setup-store-properties-export': requireSetupStorePropertiesExport, @@ -26,9 +27,13 @@ const plugin = { 'prefer-use-store-naming': preferUseStoreNaming, 'no-store-in-computed': noStoreInComputed, }, - configs: { - recommended: { - plugins: ['@pinia'], +} + +// Flat config export +;(plugin as any).configs = { + recommended: [ + { + plugins: { '@pinia': plugin as any }, rules: { '@pinia/require-setup-store-properties-export': 'error', '@pinia/no-circular-store-dependencies': 'warn', @@ -36,7 +41,7 @@ const plugin = { '@pinia/no-store-in-computed': 'error', }, }, - }, + ], } export default plugin diff --git a/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts b/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts index f1d74d2e46..02b2baef6c 100644 --- a/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts +++ b/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts @@ -49,9 +49,14 @@ ruleTester.run('prefer-use-store-naming', preferUseStoreNaming, { errors: [ { messageId: 'invalidNaming', + suggestions: [ + { + desc: 'Rename to "useDataStore"', + output: `export const useDataStore = defineStore('data', () => {})`, + }, + ], }, ], - output: `export const useDataStore = defineStore('data', () => {})`, }, // Missing 'Store' suffix { @@ -64,14 +69,19 @@ ruleTester.run('prefer-use-store-naming', preferUseStoreNaming, { errors: [ { messageId: 'invalidNaming', - }, - ], - output: ` + suggestions: [ + { + desc: 'Rename to "useUserStore"', + output: ` export const useUserStore = defineStore('user', () => { const name = ref('John') return { name } }) `, + }, + ], + }, + ], }, // Completely wrong naming { @@ -84,14 +94,19 @@ ruleTester.run('prefer-use-store-naming', preferUseStoreNaming, { errors: [ { messageId: 'invalidNaming', - }, - ], - output: ` + suggestions: [ + { + desc: 'Rename to "useUserStore"', + output: ` export const useUserStore = defineStore('user', () => { const name = ref('John') return { name } }) `, + }, + ], + }, + ], }, // Kebab-case store ID { @@ -104,14 +119,19 @@ ruleTester.run('prefer-use-store-naming', preferUseStoreNaming, { errors: [ { messageId: 'invalidNaming', - }, - ], - output: ` + suggestions: [ + { + desc: 'Rename to "useShoppingCartStore"', + output: ` export const useShoppingCartStore = defineStore('shopping-cart', () => { const items = ref([]) return { items } }) `, + }, + ], + }, + ], }, ], }) @@ -144,14 +164,19 @@ ruleTester.run( errors: [ { messageId: 'invalidNaming', - }, - ], - output: ` + suggestions: [ + { + desc: 'Rename to "createUserRepository"', + output: ` export const createUserRepository = defineStore('user', () => { const name = ref('John') return { name } }) `, + }, + ], + }, + ], }, ], } diff --git a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts index b42154fb58..981c950edb 100644 --- a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts +++ b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts @@ -3,7 +3,11 @@ * @author Eduardo San Martin Morote */ -import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { + ESLintUtils, + type TSESTree, + type TSESLint, +} from '@typescript-eslint/utils' import { isDefineStoreCall, isSetupStore, @@ -34,13 +38,14 @@ export const noCircularStoreDependencies = createRule({ circularDependency: 'Potential circular dependency detected: store "{{currentStore}}" uses "{{usedStore}}"', setupCircularDependency: - 'Avoid using other stores directly in setup function body. Use them in computed properties or actions instead.', + 'Avoid using other stores directly in setup function body. Use them in actions instead.', }, }, defaultOptions: [], create(context) { const storeUsages = new Map() // currentStore -> [usedStores] - let currentStoreName: string | null = null + const usageGraph = new Map>() // currentStore -> usedStore -> nodes + const storeStack: string[] = [] return { CallExpression(node: TSESTree.CallExpression) { @@ -52,28 +57,29 @@ export const noCircularStoreDependencies = createRule({ parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' ) { - currentStoreName = parent.id.name + const currentStoreName = parent.id.name + storeStack.push(currentStoreName) // Initialize usage tracking for this store if (!storeUsages.has(currentStoreName)) { storeUsages.set(currentStoreName, []) } + if (!usageGraph.has(currentStoreName)) { + usageGraph.set(currentStoreName, new Map()) + } // Check for store usage in setup function if (isSetupStore(node)) { const setupFunction = getSetupFunction(node) if (setupFunction) { - checkSetupFunctionForStoreUsage( - setupFunction, - currentStoreName, - context - ) + checkSetupFunctionForStoreUsage(setupFunction, context) } } } } // Track store usage calls + const currentStoreName = storeStack[storeStack.length - 1] if (isStoreUsage(node) && currentStoreName) { const usedStoreName = getStoreNameFromUsage(node) if (usedStoreName && usedStoreName !== currentStoreName) { @@ -82,6 +88,11 @@ export const noCircularStoreDependencies = createRule({ usages.push(usedStoreName) storeUsages.set(currentStoreName, usages) } + // record node for later reporting + const edges = usageGraph.get(currentStoreName)! + const nodes = edges.get(usedStoreName) ?? [] + nodes.push(node) + edges.set(usedStoreName, nodes) // Check for immediate circular dependency const usedStoreUsages = storeUsages.get(usedStoreName) || [] @@ -99,9 +110,15 @@ export const noCircularStoreDependencies = createRule({ } }, + 'CallExpression:exit'(node: TSESTree.CallExpression) { + if (isDefineStoreCall(node)) { + storeStack.pop() + } + }, + 'Program:exit'() { // Check for indirect circular dependencies - checkIndirectCircularDependencies(storeUsages, context) + checkIndirectCircularDependencies(usageGraph, context) }, } }, @@ -112,8 +129,10 @@ export const noCircularStoreDependencies = createRule({ */ function checkSetupFunctionForStoreUsage( setupFunction: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, - currentStoreName: string, - context: any + context: TSESLint.RuleContext< + 'circularDependency' | 'setupCircularDependency', + [] + > ) { if (setupFunction.body.type !== 'BlockStatement') { return @@ -150,40 +169,47 @@ function checkSetupFunctionForStoreUsage( * Checks for indirect circular dependencies (A -> B -> C -> A) */ function checkIndirectCircularDependencies( - storeUsages: Map, - context: any + usageGraph: Map>, + context: TSESLint.RuleContext< + 'circularDependency' | 'setupCircularDependency', + [] + > ) { const visited = new Set() - const recursionStack = new Set() - - function hasCycle(store: string, path: string[] = []): boolean { - if (recursionStack.has(store)) { - // Found a cycle - return true - } - - if (visited.has(store)) { - return false - } + const inPath = new Set() + const path: string[] = [] + const reported = new Set() // "A->B" + const dfs = (store: string) => { visited.add(store) - recursionStack.add(store) - - const dependencies = storeUsages.get(store) || [] - for (const dependency of dependencies) { - if (hasCycle(dependency, [...path, store])) { - return true + inPath.add(store) + path.push(store) + const deps = usageGraph.get(store) ?? new Map() + for (const [dep] of deps) { + if (!visited.has(dep)) dfs(dep) + if (inPath.has(dep)) { + // report the edge(s) participating in the cycle at least once + const from = store + const to = dep + const key = `${from}->${to}` + if (!reported.has(key)) { + reported.add(key) + const node = usageGraph.get(from)?.get(to)?.[0] + if (node) { + context.report({ + node, + messageId: 'circularDependency', + data: { currentStore: from, usedStore: to }, + }) + } + } } } - - recursionStack.delete(store) - return false + path.pop() + inPath.delete(store) } - // Check each store for cycles - for (const store of storeUsages.keys()) { - if (!visited.has(store)) { - hasCycle(store) - } + for (const store of usageGraph.keys()) { + if (!visited.has(store)) dfs(store) } } diff --git a/packages/eslint-plugin/src/rules/no-store-in-computed.ts b/packages/eslint-plugin/src/rules/no-store-in-computed.ts index e6ec8c0508..24044cc384 100644 --- a/packages/eslint-plugin/src/rules/no-store-in-computed.ts +++ b/packages/eslint-plugin/src/rules/no-store-in-computed.ts @@ -3,7 +3,11 @@ * @author Eduardo San Martin Morote */ -import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' +import { + ESLintUtils, + type TSESTree, + type TSESLint, +} from '@typescript-eslint/utils' import { isStoreUsage } from '../utils/store-utils' const createRule = ESLintUtils.RuleCreator( @@ -32,48 +36,63 @@ export const noStoreInComputed = createRule({ }, defaultOptions: [], create(context) { - let insideComputed = false - return { CallExpression(node: TSESTree.CallExpression) { - // Track when we enter a computed() call - if ( - node.callee.type === 'Identifier' && - node.callee.name === 'computed' && - node.arguments.length > 0 - ) { - insideComputed = true - - // Check the computed function for store usage - const computedFn = node.arguments[0] - if ( - computedFn.type === 'FunctionExpression' || - computedFn.type === 'ArrowFunctionExpression' - ) { - checkFunctionForStoreUsage(computedFn, context) - } - - insideComputed = false - } - - // Check for store usage inside computed - if (insideComputed && isStoreUsage(node)) { - context.report({ - node, - messageId: 'noStoreInComputed', - }) + if (!isComputedCall(node) || node.arguments.length === 0) return + const arg = node.arguments[0] + const getter = + arg.type === 'FunctionExpression' || + arg.type === 'ArrowFunctionExpression' + ? arg + : extractGetterFromObjectArg(arg) + if (getter) { + checkFunctionForStoreUsage(getter, context) } }, } }, }) +// Support: computed(), vue.computed(), imported alias still named 'computed' +function isComputedCall(node: TSESTree.CallExpression): boolean { + const callee = node.callee + return ( + (callee.type === 'Identifier' && callee.name === 'computed') || + (callee.type === 'MemberExpression' && + !callee.computed && + callee.property.type === 'Identifier' && + callee.property.name === 'computed') + ) +} + +function extractGetterFromObjectArg( + arg: TSESTree.Node +): TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | null { + if (arg.type !== 'ObjectExpression') return null + for (const prop of arg.properties) { + if ( + prop.type === 'Property' && + !prop.computed && + prop.key.type === 'Identifier' && + prop.key.name === 'get' + ) { + const v = prop.value + if ( + v.type === 'FunctionExpression' || + v.type === 'ArrowFunctionExpression' + ) + return v + } + } + return null +} + /** * Recursively checks a function for store usage */ function checkFunctionForStoreUsage( fn: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, - context: any + context: TSESLint.RuleContext<'noStoreInComputed', []> ) { const visited = new Set() diff --git a/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts index 501603596e..9d8e4644ed 100644 --- a/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts +++ b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts @@ -4,7 +4,7 @@ */ import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils' -import { isDefineStoreCall } from '../utils/ast-utils' +import { isDefineStoreCall, getStoreId } from '../utils/ast-utils' const createRule = ESLintUtils.RuleCreator( (name) => `https://pinia.vuejs.org/cookbook/eslint-plugin.html#${name}` @@ -24,7 +24,7 @@ export const preferUseStoreNaming = createRule({ description: 'enforce consistent store naming conventions', recommended: 'warn', }, - fixable: 'code', + hasSuggestions: true, schema: [ { type: 'object', @@ -43,7 +43,7 @@ export const preferUseStoreNaming = createRule({ ], messages: { invalidNaming: - 'Store function should follow the naming pattern "{{prefix}}{{name}}{{suffix}}"', + 'Store function should follow the naming pattern "{{expected}}"', }, }, defaultOptions: [{ prefix: 'use', suffix: 'Store' }], @@ -64,12 +64,8 @@ export const preferUseStoreNaming = createRule({ if (!storeName.startsWith(prefix) || !storeName.endsWith(suffix)) { // Extract the core name from store ID if available let suggestedName = storeName - if ( - node.init.arguments.length > 0 && - node.init.arguments[0].type === 'Literal' && - typeof node.init.arguments[0].value === 'string' - ) { - const storeId = node.init.arguments[0].value + const storeId = getStoreId(node.init) + if (storeId) { // Convert kebab-case or snake_case to PascalCase const coreName = storeId .split(/[-_]/) @@ -95,13 +91,16 @@ export const preferUseStoreNaming = createRule({ node: node.id, messageId: 'invalidNaming', data: { - prefix, - name: '{{Name}}', - suffix, - }, - fix(fixer) { - return fixer.replaceText(node.id, suggestedName) + expected: suggestedName, }, + suggest: [ + { + desc: `Rename to "${suggestedName}"`, + fix(fixer) { + return fixer.replaceText(node.id, suggestedName) + }, + }, + ], }) } } diff --git a/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts index 4767b73e92..e7fdabdbe0 100644 --- a/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts +++ b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts @@ -9,8 +9,9 @@ import { isSetupStore, getSetupFunction, extractDeclarations, - extractReturnProperties, + extractReturnIdentifiers, findReturnStatement, + hasSpreadInReturn, } from '../utils/ast-utils' const createRule = ESLintUtils.RuleCreator( @@ -76,12 +77,17 @@ export const requireSetupStorePropertiesExport = createRule({ return } - // Extract exported properties - const exportedProperties = extractReturnProperties(returnStatement) + // Extract exported identifiers + const exportedIdentifiers = extractReturnIdentifiers(returnStatement) + + // Be lenient when spreads are present to avoid false positives + if (hasSpreadInReturn(returnStatement)) { + return + } // Find missing exports const missingExports = allDeclared.filter( - (name) => !exportedProperties.includes(name) + (name) => !exportedIdentifiers.includes(name) ) if (missingExports.length > 0) { diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts index 07acf9aac6..d6a97159b4 100644 --- a/packages/eslint-plugin/src/utils/ast-utils.ts +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -12,11 +12,44 @@ export function isDefineStoreCall( ): node is TSESTree.CallExpression { return ( node.type === 'CallExpression' && - node.callee.type === 'Identifier' && - node.callee.name === 'defineStore' + ((node.callee.type === 'Identifier' && + node.callee.name === 'defineStore') || + (node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'defineStore')) ) } +/** + * Extracts store ID from defineStore call arguments + */ +export function getStoreId(node: TSESTree.CallExpression): string | null { + if (!isDefineStoreCall(node)) return null + + const firstArg = node.arguments[0] + if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') { + return firstArg.value + } + + if (firstArg?.type === 'ObjectExpression') { + for (const prop of firstArg.properties) { + if ( + prop.type === 'Property' && + !prop.computed && + prop.key.type === 'Identifier' && + prop.key.name === 'id' && + prop.value.type === 'Literal' && + typeof prop.value.value === 'string' + ) { + return prop.value.value + } + } + } + + return null +} + /** * Checks if a call expression is a setup store (has a function as second argument) */ @@ -62,9 +95,7 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { for (const statement of body.body) { if (statement.type === 'VariableDeclaration') { for (const declarator of statement.declarations) { - if (declarator.id.type === 'Identifier') { - variables.push(declarator.id.name) - } + extractIdentifiersFromPattern(declarator.id, variables) } } else if (statement.type === 'FunctionDeclaration' && statement.id) { functions.push(statement.id.name) @@ -75,7 +106,43 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { } /** - * Extracts properties from a return statement object + * Extracts identifier names from patterns (handles destructuring) + */ +function extractIdentifiersFromPattern( + pattern: TSESTree.BindingName, + identifiers: string[] +): void { + switch (pattern.type) { + case 'Identifier': + identifiers.push(pattern.name) + break + case 'ObjectPattern': + for (const prop of pattern.properties) { + if (prop.type === 'Property') { + extractIdentifiersFromPattern(prop.value, identifiers) + } else if (prop.type === 'RestElement') { + extractIdentifiersFromPattern(prop.argument, identifiers) + } + } + break + case 'ArrayPattern': + for (const element of pattern.elements) { + if (element) { + extractIdentifiersFromPattern(element, identifiers) + } + } + break + case 'RestElement': + extractIdentifiersFromPattern(pattern.argument, identifiers) + break + case 'AssignmentPattern': + extractIdentifiersFromPattern(pattern.left, identifiers) + break + } +} + +/** + * Extracts properties from a return statement object (keys only) */ export function extractReturnProperties( returnStatement: TSESTree.ReturnStatement @@ -101,6 +168,56 @@ export function extractReturnProperties( return properties } +/** + * Extracts identifiers being returned from a return statement object + * This handles aliasing: return { total: count } returns ['count'] + */ +export function extractReturnIdentifiers( + returnStatement: TSESTree.ReturnStatement +): string[] { + if ( + !returnStatement.argument || + returnStatement.argument.type !== 'ObjectExpression' + ) { + return [] + } + + const identifiers: string[] = [] + + for (const prop of returnStatement.argument.properties) { + if (prop.type === 'Property') { + if (prop.shorthand && prop.key.type === 'Identifier') { + // Shorthand property: { count } -> count + identifiers.push(prop.key.name) + } else if (prop.value.type === 'Identifier') { + // Aliased property: { total: count } -> count + identifiers.push(prop.value.name) + } + } + // Skip spread elements as we can't determine what's being spread + } + + return identifiers +} + +/** + * Checks if a return statement has spread elements + */ +export function hasSpreadInReturn( + returnStatement: TSESTree.ReturnStatement +): boolean { + if ( + !returnStatement.argument || + returnStatement.argument.type !== 'ObjectExpression' + ) { + return false + } + + return returnStatement.argument.properties.some( + (prop) => prop.type === 'SpreadElement' + ) +} + /** * Finds the return statement in a function body */ diff --git a/packages/eslint-plugin/test_output.txt b/packages/eslint-plugin/test_output.txt deleted file mode 100644 index f5bcab74b2..0000000000 --- a/packages/eslint-plugin/test_output.txt +++ /dev/null @@ -1,190 +0,0 @@ - -> @pinia/eslint-plugin@1.0.0 test D:\pych_projects\pinia\packages\eslint-plugin -> vitest run - - - RUN  v3.2.4 D:/pych_projects/pinia/packages/eslint-plugin - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > valid >  - export const useUserStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: useUserStore prefix: use suffix: Store -Checking: useUserStore startsWithPrefix: true endsWithSuffix: true - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > valid >  - export const useShoppingCartStore = defineStore('shopping-cart', () => { - const items = ref([]) - return { items } - }) - -Found store definition: useShoppingCartStore prefix: use suffix: Store -Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  - export const userStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: userStore prefix: use suffix: Store -Checking: userStore startsWithPrefix: true endsWithSuffix: true - - тЬУ src/rules/__tests__/no-store-in-computed.test.ts (6 tests) 57ms -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  - export const useUser = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: useUser prefix: use suffix: Store -Checking: useUser startsWithPrefix: true endsWithSuffix: false -Naming violation detected for: useUser -Found store definition: useUserStore prefix: use suffix: Store -Checking: useUserStore startsWithPrefix: true endsWithSuffix: true -Found store definition: useUserStore prefix: use suffix: Store -Checking: useUserStore startsWithPrefix: true endsWithSuffix: true - - тЬУ src/rules/__tests__/no-circular-store-dependencies.test.ts (6 tests) 58ms -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  - export const myStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: myStore prefix: use suffix: Store -Checking: myStore startsWithPrefix: false endsWithSuffix: true -Naming violation detected for: myStore -Found store definition: useUserStore prefix: use suffix: Store -Checking: useUserStore startsWithPrefix: true endsWithSuffix: true -Found store definition: useUserStore prefix: use suffix: Store -Checking: useUserStore startsWithPrefix: true endsWithSuffix: true - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid >  - export const myStore = defineStore('shopping-cart', () => { - const items = ref([]) - return { items } - }) - -Found store definition: myStore prefix: use suffix: Store -Checking: myStore startsWithPrefix: false endsWithSuffix: true -Naming violation detected for: myStore -Found store definition: useShoppingCartStore prefix: use suffix: Store -Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true -Found store definition: useShoppingCartStore prefix: use suffix: Store -Checking: useShoppingCartStore startsWithPrefix: true endsWithSuffix: true - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming with custom options > valid >  - export const createUserRepository = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: createUserRepository prefix: create suffix: Repository -Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true - -stdout | src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming with custom options > invalid >  - export const useUserStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -Found store definition: useUserStore prefix: create suffix: Repository -Checking: useUserStore startsWithPrefix: false endsWithSuffix: false -Naming violation detected for: useUserStore -Found store definition: createUserRepository prefix: create suffix: Repository -Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true -Found store definition: createUserRepository prefix: create suffix: Repository -Checking: createUserRepository startsWithPrefix: true endsWithSuffix: true - - тЬУ src/rules/__tests__/require-setup-store-properties-export.test.ts (8 tests) 72ms - тЭп src/rules/__tests__/prefer-use-store-naming.test.ts (9 tests | 1 failed) 78ms - тЬУ prefer-use-store-naming > valid >  - export const useUserStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  33ms - тЬУ prefer-use-store-naming > valid >  - export const useShoppingCartStore = defineStore('shopping-cart', () => { - const items = ref([]) - return { items } - }) -  5ms - тЬУ prefer-use-store-naming > valid >  - const myVariable = someFunction() -  2ms - ├Ч prefer-use-store-naming > invalid >  - export const userStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  7ms - тЖТ Should have 1 error but had 0: [] - -0 !== 1 - - тЬУ prefer-use-store-naming > invalid >  - export const useUser = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  11ms - тЬУ prefer-use-store-naming > invalid >  - export const myStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  8ms - тЬУ prefer-use-store-naming > invalid >  - export const myStore = defineStore('shopping-cart', () => { - const items = ref([]) - return { items } - }) -  4ms - тЬУ prefer-use-store-naming with custom options > valid >  - export const createUserRepository = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  2ms - тЬУ prefer-use-store-naming with custom options > invalid >  - export const useUserStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) -  4ms - -тОптОптОптОптОптОптОп Failed Tests 1 тОптОптОптОптОптОптОп - - FAIL src/rules/__tests__/prefer-use-store-naming.test.ts > prefer-use-store-naming > invalid > - export const userStore = defineStore('user', () => { - const name = ref('John') - return { name } - }) - -AssertionError: Should have 1 error but had 0: [] - -0 !== 1 - - -- Expected -+ Received - -- 1 -+ 0 - - тЭп RuleTester.#testInvalidTemplate ../../node_modules/.pnpm/@typescript-eslint+rule-tes_4d91085b7cef78db45b5c8037c2a1f8c/node_modules/@typescript-eslint/rule-tester/dist/RuleTester.js:706:35 - тЭп ../../node_modules/.pnpm/@typescript-eslint+rule-tes_4d91085b7cef78db45b5c8037c2a1f8c/node_modules/@typescript-eslint/rule-tester/dist/RuleTester.js:448:58 - -тОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОптОп[1/1]тОп - - - Test Files  1 failed | 3 passed (4) - Tests  1 failed | 28 passed (29) - Start at  09:21:23 - Duration  1.08s (transform 110ms, setup 0ms, collect 2.47s, tests 266ms, environment 1ms, prepare 607ms) - -тАЙELIFECYCLEтАЙ Test failed. See above for more details. diff --git a/packages/eslint-plugin/tsup.config.ts b/packages/eslint-plugin/tsup.config.ts index 22abee19ef..c1dd3dbaa0 100644 --- a/packages/eslint-plugin/tsup.config.ts +++ b/packages/eslint-plugin/tsup.config.ts @@ -5,8 +5,11 @@ export default defineConfig({ format: ['cjs', 'esm'], dts: true, clean: true, - external: ['eslint'], - target: 'node14', + external: ['eslint', '@typescript-eslint/utils'], + target: 'node18', splitting: false, sourcemap: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.js', + }), }) From bf944aa67067d7914611dabfd0c9b63ffed6239b Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Tue, 16 Sep 2025 22:14:57 +0300 Subject: [PATCH 3/7] fix(eslint-plugin): address additional CodeRabbit review comments - Fix package.json files field to include .d.mts and .map files - Fix DFS algorithm in circular dependency detection by reordering conditional checks - Enhance AST parsing with recursive traversal for nested declarations in blocks, conditionals, loops - Improve findReturnStatement to handle multiple returns and find main object return - Add findAllReturnStatements function for comprehensive return statement detection These changes improve the robustness of AST parsing and fix potential issues with circular dependency detection while maintaining backward compatibility. --- packages/eslint-plugin/package.json | 4 +- .../rules/no-circular-store-dependencies.ts | 3 +- packages/eslint-plugin/src/utils/ast-utils.ts | 162 ++++++++++++++++-- 3 files changed, 154 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index d24a46f000..5bb9023827 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -37,7 +37,9 @@ "files": [ "dist/*.js", "dist/*.mjs", - "dist/*.d.ts" + "dist/*.d.ts", + "dist/*.d.mts", + "dist/*.map" ], "scripts": { "build": "tsup", diff --git a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts index 981c950edb..b26f8647e3 100644 --- a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts +++ b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts @@ -186,7 +186,6 @@ function checkIndirectCircularDependencies( path.push(store) const deps = usageGraph.get(store) ?? new Map() for (const [dep] of deps) { - if (!visited.has(dep)) dfs(dep) if (inPath.has(dep)) { // report the edge(s) participating in the cycle at least once const from = store @@ -203,6 +202,8 @@ function checkIndirectCircularDependencies( }) } } + } else if (!visited.has(dep)) { + dfs(dep) } } path.pop() diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts index d6a97159b4..c7c095722c 100644 --- a/packages/eslint-plugin/src/utils/ast-utils.ts +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -83,7 +83,7 @@ export function getSetupFunction( } /** - * Extracts variable and function declarations from a function body + * Extracts variable and function declarations from a function body (recursive) */ export function extractDeclarations(body: TSESTree.BlockStatement): { variables: string[] @@ -92,16 +92,65 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { const variables: string[] = [] const functions: string[] = [] - for (const statement of body.body) { - if (statement.type === 'VariableDeclaration') { - for (const declarator of statement.declarations) { - extractIdentifiersFromPattern(declarator.id, variables) - } - } else if (statement.type === 'FunctionDeclaration' && statement.id) { - functions.push(statement.id.name) + function traverse(node: TSESTree.Node): void { + switch (node.type) { + case 'VariableDeclaration': + for (const declarator of node.declarations) { + extractIdentifiersFromPattern(declarator.id, variables) + } + break + case 'FunctionDeclaration': + if (node.id) { + functions.push(node.id.name) + } + break + case 'BlockStatement': + for (const statement of node.body) { + traverse(statement) + } + break + case 'IfStatement': + traverse(node.consequent) + if (node.alternate) { + traverse(node.alternate) + } + break + case 'ForStatement': + case 'ForInStatement': + case 'ForOfStatement': + if (node.body) { + traverse(node.body) + } + break + case 'WhileStatement': + case 'DoWhileStatement': + traverse(node.body) + break + case 'SwitchStatement': + for (const switchCase of node.cases) { + for (const statement of switchCase.consequent) { + traverse(statement) + } + } + break + case 'TryStatement': + traverse(node.block) + if (node.handler) { + traverse(node.handler.body) + } + if (node.finalizer) { + traverse(node.finalizer) + } + break + case 'WithStatement': + traverse(node.body) + break + // For other statement types, we don't need to traverse deeper + // as they don't contain variable/function declarations } } + traverse(body) return { variables, functions } } @@ -219,15 +268,102 @@ export function hasSpreadInReturn( } /** - * Finds the return statement in a function body + * Finds all return statements in a function body (recursive) + */ +export function findAllReturnStatements( + body: TSESTree.BlockStatement +): TSESTree.ReturnStatement[] { + const returnStatements: TSESTree.ReturnStatement[] = [] + + function traverse(node: TSESTree.Node): void { + switch (node.type) { + case 'ReturnStatement': + returnStatements.push(node) + break + case 'BlockStatement': + for (const statement of node.body) { + traverse(statement) + } + break + case 'IfStatement': + traverse(node.consequent) + if (node.alternate) { + traverse(node.alternate) + } + break + case 'ForStatement': + case 'ForInStatement': + case 'ForOfStatement': + if (node.body) { + traverse(node.body) + } + break + case 'WhileStatement': + case 'DoWhileStatement': + traverse(node.body) + break + case 'SwitchStatement': + for (const switchCase of node.cases) { + for (const statement of switchCase.consequent) { + traverse(statement) + } + } + break + case 'TryStatement': + traverse(node.block) + if (node.handler) { + traverse(node.handler.body) + } + if (node.finalizer) { + traverse(node.finalizer) + } + break + case 'WithStatement': + traverse(node.body) + break + // For function declarations/expressions, we don't traverse into them + // as they have their own scope + case 'FunctionDeclaration': + case 'FunctionExpression': + case 'ArrowFunctionExpression': + break + // For other statement types that can contain nested statements + case 'ExpressionStatement': + case 'VariableDeclaration': + case 'ThrowStatement': + case 'BreakStatement': + case 'ContinueStatement': + case 'EmptyStatement': + case 'DebuggerStatement': + // These don't contain nested statements + break + } + } + + traverse(body) + return returnStatements +} + +/** + * Finds the main return statement in a function body (typically the last object return) */ export function findReturnStatement( body: TSESTree.BlockStatement ): TSESTree.ReturnStatement | null { - for (const statement of body.body) { - if (statement.type === 'ReturnStatement') { - return statement + const allReturns = findAllReturnStatements(body) + + if (allReturns.length === 0) { + return null + } + + // Find the last return statement that returns an object expression + for (let i = allReturns.length - 1; i >= 0; i--) { + const returnStmt = allReturns[i] + if (returnStmt.argument?.type === 'ObjectExpression') { + return returnStmt } } - return null + + // If no object return found, return the last return statement + return allReturns[allReturns.length - 1] } From 9cd4f43bdedbcb6800d3e526bf9c42d9f0d08327 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Tue, 16 Sep 2025 22:44:20 +0300 Subject: [PATCH 4/7] test(ast-utils): add comprehensive unit tests for utility functions - Add unit tests for isDefineStoreCall, getStoreId, extractDeclarations, and extractReturnProperties - Ensure support for optional chaining, template literals, and nested structures in AST utilities - Improve test coverage to handle edge cases and various argument forms --- packages/eslint-plugin/package.json | 7 +- .../src/utils/__tests__/ast-utils.test.ts | 225 ++++++++++++++++++ packages/eslint-plugin/src/utils/ast-utils.ts | 180 +++++++++++--- packages/eslint-plugin/tsup.config.ts | 2 +- 4 files changed, 378 insertions(+), 36 deletions(-) create mode 100644 packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 5bb9023827..7546f305f2 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -28,14 +28,15 @@ "sideEffects": false, "exports": { "types": "./dist/index.d.ts", - "require": "./dist/index.js", + "require": "./dist/index.cjs", "import": "./dist/index.mjs" }, - "main": "./dist/index.js", + "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ "dist/*.js", + "dist/*.cjs", "dist/*.mjs", "dist/*.d.ts", "dist/*.d.mts", @@ -61,7 +62,7 @@ "node": ">=18.18.0" }, "peerDependencies": { - "eslint": ">=8.0.0" + "eslint": ">=8.57.0 || ^9.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts new file mode 100644 index 0000000000..fdbde937eb --- /dev/null +++ b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts @@ -0,0 +1,225 @@ +/** + * @fileoverview Tests for AST utilities + */ + +import { describe, it, expect } from 'vitest' +import { parseForESLint } from '@typescript-eslint/parser' +import type { TSESTree } from '@typescript-eslint/utils' +import { + isDefineStoreCall, + getStoreId, + extractDeclarations, + extractReturnProperties, +} from '../ast-utils' + +function parseCode(code: string): TSESTree.Program { + const result = parseForESLint(code, { + ecmaVersion: 2020, + sourceType: 'module', + }) + return result.ast +} + +function findCallExpression(ast: TSESTree.Program): TSESTree.CallExpression { + let callExpression: TSESTree.CallExpression | null = null + + function traverse(node: any): void { + if (node.type === 'CallExpression') { + callExpression = node + return + } + for (const key in node) { + if (node[key] && typeof node[key] === 'object') { + if (Array.isArray(node[key])) { + node[key].forEach(traverse) + } else { + traverse(node[key]) + } + } + } + } + + traverse(ast) + return callExpression! +} + +describe('isDefineStoreCall', () => { + it('should detect direct defineStore calls', () => { + const code = 'defineStore("test", () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(isDefineStoreCall(callExpr)).toBe(true) + }) + + it('should detect member expression defineStore calls', () => { + const code = 'pinia.defineStore("test", () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(isDefineStoreCall(callExpr)).toBe(true) + }) + + it('should detect optional chaining defineStore calls', () => { + const code = 'pinia?.defineStore("test", () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(isDefineStoreCall(callExpr)).toBe(true) + }) + + it('should not detect non-defineStore calls', () => { + const code = 'someOtherFunction("test", () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(isDefineStoreCall(callExpr)).toBe(false) + }) +}) + +describe('getStoreId', () => { + it('should extract string literal IDs', () => { + const code = 'defineStore("user", () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(getStoreId(callExpr)).toBe('user') + }) + + it('should extract template literal IDs without interpolations', () => { + const code = 'defineStore(`user`, () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(getStoreId(callExpr)).toBe('user') + }) + + it('should extract IDs from object expressions with string literals', () => { + const code = 'defineStore({ id: "user" }, () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(getStoreId(callExpr)).toBe('user') + }) + + it('should extract IDs from object expressions with template literals', () => { + const code = 'defineStore({ id: `user` }, () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(getStoreId(callExpr)).toBe('user') + }) + + it('should return null for template literals with interpolations', () => { + const code = 'defineStore(`user-${suffix}`, () => {})' + const ast = parseCode(code) + const callExpr = findCallExpression(ast) + expect(getStoreId(callExpr)).toBe(null) + }) +}) + +describe('extractDeclarations', () => { + it('should extract variable declarations and deduplicate', () => { + const code = ` + function setup() { + const name = ref('test') + let count = 0 + const name = ref('duplicate') // This should be deduplicated + return { name, count } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const result = extractDeclarations(func.body!) + + expect(result.variables).toEqual(['name', 'count']) + }) + + it('should extract loop initializer declarations', () => { + const code = ` + function setup() { + for (let i = 0; i < 10; i++) { + console.log(i) + } + for (const item of items) { + console.log(item) + } + return {} + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const result = extractDeclarations(func.body!) + + expect(result.variables).toContain('i') + expect(result.variables).toContain('item') + }) + + it('should extract catch clause parameters', () => { + const code = ` + function setup() { + try { + doSomething() + } catch (error) { + console.log(error) + } + return {} + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const result = extractDeclarations(func.body!) + + expect(result.variables).toContain('error') + }) +}) + +describe('extractReturnProperties', () => { + it('should extract identifier property keys', () => { + const code = ` + function setup() { + return { name, count, total } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement + const result = extractReturnProperties(returnStmt) + + expect(result).toEqual(['name', 'count', 'total']) + }) + + it('should extract quoted string property keys', () => { + const code = ` + function setup() { + return { "name": value, 'count': value2 } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement + const result = extractReturnProperties(returnStmt) + + expect(result).toEqual(['name', 'count']) + }) + + it('should extract template literal property keys without interpolations', () => { + const code = ` + function setup() { + return { [\`name\`]: value, [\`count\`]: value2 } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement + const result = extractReturnProperties(returnStmt) + + expect(result).toEqual(['name', 'count']) + }) + + it('should handle MemberExpression returns', () => { + const code = ` + function setup() { + return someObject.property + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement + const result = extractReturnProperties(returnStmt) + + expect(result).toEqual([]) + }) +}) diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts index c7c095722c..0561928f1b 100644 --- a/packages/eslint-plugin/src/utils/ast-utils.ts +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -6,43 +6,94 @@ import type { TSESTree } from '@typescript-eslint/utils' /** * Checks if a node is a call expression to `defineStore` + * Handles optional chaining and chain expressions (e.g., pinia?.defineStore(...)) */ export function isDefineStoreCall( node: TSESTree.Node ): node is TSESTree.CallExpression { - return ( - node.type === 'CallExpression' && - ((node.callee.type === 'Identifier' && - node.callee.name === 'defineStore') || - (node.callee.type === 'MemberExpression' && - !node.callee.computed && - node.callee.property.type === 'Identifier' && - node.callee.property.name === 'defineStore')) - ) + if (node.type !== 'CallExpression') { + return false + } + + return isDefineStoreCallee(node.callee) +} + +/** + * Helper function to check if a callee is a defineStore call + * Handles various patterns including optional chaining + */ +function isDefineStoreCallee( + callee: TSESTree.CallExpression['callee'] +): boolean { + // Direct call: defineStore(...) + if (callee.type === 'Identifier' && callee.name === 'defineStore') { + return true + } + + // Member expression: pinia.defineStore(...) + if ( + callee.type === 'MemberExpression' && + !callee.computed && + callee.property.type === 'Identifier' && + callee.property.name === 'defineStore' + ) { + return true + } + + // Chain expression (optional chaining): pinia?.defineStore(...) + if (callee.type === 'ChainExpression') { + return isDefineStoreCallee(callee.expression) + } + + return false } /** * Extracts store ID from defineStore call arguments + * Handles template literals without interpolations */ export function getStoreId(node: TSESTree.CallExpression): string | null { if (!isDefineStoreCall(node)) return null const firstArg = node.arguments[0] + + // Handle string literals if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') { return firstArg.value } + // Handle template literals without interpolations + if ( + firstArg?.type === 'TemplateLiteral' && + firstArg.expressions.length === 0 + ) { + // Template literal with no interpolations is just a string + return firstArg.quasis[0]?.value.cooked || null + } + + // Handle object expression with id property if (firstArg?.type === 'ObjectExpression') { for (const prop of firstArg.properties) { if ( prop.type === 'Property' && !prop.computed && prop.key.type === 'Identifier' && - prop.key.name === 'id' && - prop.value.type === 'Literal' && - typeof prop.value.value === 'string' + prop.key.name === 'id' ) { - return prop.value.value + // Handle string literal value + if ( + prop.value.type === 'Literal' && + typeof prop.value.value === 'string' + ) { + return prop.value.value + } + // Handle template literal value without interpolations + if ( + prop.value.type === 'TemplateLiteral' && + prop.value.expressions.length === 0 + ) { + return prop.value.quasis[0]?.value.cooked || null + } } } } @@ -84,24 +135,25 @@ export function getSetupFunction( /** * Extracts variable and function declarations from a function body (recursive) + * Captures loop-initializer declarations and de-duplicates outputs */ export function extractDeclarations(body: TSESTree.BlockStatement): { variables: string[] functions: string[] } { - const variables: string[] = [] - const functions: string[] = [] + const variableSet = new Set() + const functionSet = new Set() function traverse(node: TSESTree.Node): void { switch (node.type) { case 'VariableDeclaration': for (const declarator of node.declarations) { - extractIdentifiersFromPattern(declarator.id, variables) + extractIdentifiersFromPattern(declarator.id, variableSet) } break case 'FunctionDeclaration': if (node.id) { - functions.push(node.id.name) + functionSet.add(node.id.name) } break case 'BlockStatement': @@ -116,8 +168,24 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { } break case 'ForStatement': + // Handle loop initializer declarations (e.g., for (let i = 0; ...)) + if (node.init && node.init.type === 'VariableDeclaration') { + for (const declarator of node.init.declarations) { + extractIdentifiersFromPattern(declarator.id, variableSet) + } + } + if (node.body) { + traverse(node.body) + } + break case 'ForInStatement': case 'ForOfStatement': + // Handle loop variable declarations (e.g., for (const item of items)) + if (node.left.type === 'VariableDeclaration') { + for (const declarator of node.left.declarations) { + extractIdentifiersFromPattern(declarator.id, variableSet) + } + } if (node.body) { traverse(node.body) } @@ -136,6 +204,10 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { case 'TryStatement': traverse(node.block) if (node.handler) { + // Handle catch clause parameter (e.g., catch (error)) + if (node.handler.param) { + extractIdentifiersFromPattern(node.handler.param, variableSet) + } traverse(node.handler.body) } if (node.finalizer) { @@ -151,7 +223,10 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { } traverse(body) - return { variables, functions } + return { + variables: Array.from(variableSet), + functions: Array.from(functionSet), + } } /** @@ -159,11 +234,11 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { */ function extractIdentifiersFromPattern( pattern: TSESTree.BindingName, - identifiers: string[] + identifiers: Set ): void { switch (pattern.type) { case 'Identifier': - identifiers.push(pattern.name) + identifiers.add(pattern.name) break case 'ObjectPattern': for (const prop of pattern.properties) { @@ -192,29 +267,70 @@ function extractIdentifiersFromPattern( /** * Extracts properties from a return statement object (keys only) + * Handles quoted keys, literal property keys, and MemberExpression returns */ export function extractReturnProperties( returnStatement: TSESTree.ReturnStatement ): string[] { - if ( - !returnStatement.argument || - returnStatement.argument.type !== 'ObjectExpression' - ) { + if (!returnStatement.argument) { return [] } - const properties: string[] = [] + // Handle object expression returns + if (returnStatement.argument.type === 'ObjectExpression') { + const properties: string[] = [] - for (const prop of returnStatement.argument.properties) { - if (prop.type === 'Property' && prop.key.type === 'Identifier') { - properties.push(prop.key.name) - } else if (prop.type === 'SpreadElement') { - // Handle spread elements - we can't easily determine what's being spread - // so we'll be more lenient in this case + for (const prop of returnStatement.argument.properties) { + if (prop.type === 'Property') { + // Handle identifier keys: { name: ... } + if (prop.key.type === 'Identifier' && !prop.computed) { + properties.push(prop.key.name) + } + // Handle string literal keys: { "name": ... } or { 'name': ... } + else if ( + prop.key.type === 'Literal' && + typeof prop.key.value === 'string' + ) { + properties.push(prop.key.value) + } + // Handle template literal keys without interpolations: { `name`: ... } + else if ( + prop.key.type === 'TemplateLiteral' && + prop.key.expressions.length === 0 + ) { + const value = prop.key.quasis[0]?.value.cooked + if (value) { + properties.push(value) + } + } + // Handle computed property keys with template literals: { [`name`]: ... } + else if ( + prop.computed && + prop.key.type === 'TemplateLiteral' && + prop.key.expressions.length === 0 + ) { + const value = prop.key.quasis[0]?.value.cooked + if (value) { + properties.push(value) + } + } + } else if (prop.type === 'SpreadElement') { + // Handle spread elements - we can't easily determine what's being spread + // so we'll be more lenient in this case + } } + + return properties + } + + // Handle MemberExpression returns (e.g., return someObject.property) + if (returnStatement.argument.type === 'MemberExpression') { + // For MemberExpression, we can't determine the exact properties + // but we can note that it's a dynamic return + return [] } - return properties + return [] } /** diff --git a/packages/eslint-plugin/tsup.config.ts b/packages/eslint-plugin/tsup.config.ts index c1dd3dbaa0..98a7df973b 100644 --- a/packages/eslint-plugin/tsup.config.ts +++ b/packages/eslint-plugin/tsup.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ splitting: false, sourcemap: true, outExtension: ({ format }) => ({ - js: format === 'esm' ? '.mjs' : '.js', + js: format === 'esm' ? '.mjs' : '.cjs', }), }) From f0c3db3990180d1231052f429fd0af2573678f25 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 18 Sep 2025 08:43:32 +0300 Subject: [PATCH 5/7] Update packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts index fdbde937eb..9d1353b43c 100644 --- a/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts +++ b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts @@ -114,9 +114,9 @@ describe('extractDeclarations', () => { it('should extract variable declarations and deduplicate', () => { const code = ` function setup() { - const name = ref('test') + var name = ref('test') let count = 0 - const name = ref('duplicate') // This should be deduplicated + var name = ref('duplicate') // This should be deduplicated return { name, count } } ` From 91536543d3763fdd5cf2c203e2d1586f3693e9d2 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 18 Sep 2025 08:45:27 +0300 Subject: [PATCH 6/7] Update packages/eslint-plugin/src/utils/ast-utils.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/eslint-plugin/src/utils/ast-utils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts index 0561928f1b..bf57dc79a8 100644 --- a/packages/eslint-plugin/src/utils/ast-utils.ts +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -293,16 +293,6 @@ export function extractReturnProperties( ) { properties.push(prop.key.value) } - // Handle template literal keys without interpolations: { `name`: ... } - else if ( - prop.key.type === 'TemplateLiteral' && - prop.key.expressions.length === 0 - ) { - const value = prop.key.quasis[0]?.value.cooked - if (value) { - properties.push(value) - } - } // Handle computed property keys with template literals: { [`name`]: ... } else if ( prop.computed && From bab6a8c11baf57fa23aee01997cbefe5302ee429 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 18 Sep 2025 09:53:10 +0300 Subject: [PATCH 7/7] refactor(ast-utils, rules): enhance utility extraction and test coverage - Rename extractDeclarations to extractTopLevelDeclarations for clarity and scope consistency - Add extractReturnIdentifiers for improved return property identifier detection - Extend unit tests for AST utilities to cover edge cases and TS/JS wrappers - Update rules to ensure consistent usage of extractTopLevelDeclarations and identifiers handling - Improve test coverage and robustness of rule logic --- packages/eslint-plugin/package.json | 5 +- .../no-circular-store-dependencies.test.ts | 3 +- .../__tests__/no-store-in-computed.test.ts | 3 +- .../rules/no-circular-store-dependencies.ts | 1 - .../src/rules/no-store-in-computed.ts | 1 - .../src/rules/prefer-use-store-naming.ts | 5 +- .../require-setup-store-properties-export.ts | 9 +- .../src/utils/__tests__/ast-utils.test.ts | 52 ++++- packages/eslint-plugin/src/utils/ast-utils.ts | 130 ++++++++++-- pnpm-lock.yaml | 192 ++++++++++++++---- 10 files changed, 331 insertions(+), 70 deletions(-) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 7546f305f2..cc7b9d305b 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -45,17 +45,20 @@ "scripts": { "build": "tsup", "test": "vitest run", + "test:coverage": "vitest --coverage", "test:watch": "vitest", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l @pinia/eslint-plugin -r 1" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@typescript-eslint/parser": "^8.35.1", "@typescript-eslint/rule-tester": "^8.35.1", "@typescript-eslint/utils": "^8.35.1", - "eslint": "^9.0.0", + "eslint": "^9.35.0", "pinia": "workspace:*", "tsup": "^8.5.0", "typescript": "~5.8.3", + "typescript-eslint": "^8.44.0", "vitest": "^3.2.4" }, "engines": { diff --git a/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts b/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts index fde87210ac..70610613f6 100644 --- a/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts +++ b/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts @@ -4,10 +4,11 @@ import { RuleTester } from '@typescript-eslint/rule-tester' import { noCircularStoreDependencies } from '../no-circular-store-dependencies' +import * as parser from '@typescript-eslint/parser' const ruleTester = new RuleTester({ languageOptions: { - parser: require('@typescript-eslint/parser'), + parser, parserOptions: { ecmaVersion: 2020, sourceType: 'module', diff --git a/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts b/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts index 17ac49b66b..2d4f2b743d 100644 --- a/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts +++ b/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts @@ -4,10 +4,11 @@ import { RuleTester } from '@typescript-eslint/rule-tester' import { noStoreInComputed } from '../no-store-in-computed' +import * as parser from '@typescript-eslint/parser' const ruleTester = new RuleTester({ languageOptions: { - parser: require('@typescript-eslint/parser'), + parser, parserOptions: { ecmaVersion: 2020, sourceType: 'module', diff --git a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts index b26f8647e3..6ded8f13de 100644 --- a/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts +++ b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts @@ -31,7 +31,6 @@ export const noCircularStoreDependencies = createRule({ type: 'problem', docs: { description: 'disallow circular dependencies between stores', - recommended: 'warn', }, schema: [], messages: { diff --git a/packages/eslint-plugin/src/rules/no-store-in-computed.ts b/packages/eslint-plugin/src/rules/no-store-in-computed.ts index 24044cc384..1c0798828f 100644 --- a/packages/eslint-plugin/src/rules/no-store-in-computed.ts +++ b/packages/eslint-plugin/src/rules/no-store-in-computed.ts @@ -26,7 +26,6 @@ export const noStoreInComputed = createRule({ type: 'problem', docs: { description: 'disallow store instantiation in computed properties', - recommended: 'error', }, schema: [], messages: { diff --git a/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts index 9d8e4644ed..448f4660f2 100644 --- a/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts +++ b/packages/eslint-plugin/src/rules/prefer-use-store-naming.ts @@ -22,7 +22,6 @@ export const preferUseStoreNaming = createRule({ type: 'suggestion', docs: { description: 'enforce consistent store naming conventions', - recommended: 'warn', }, hasSuggestions: true, schema: [ @@ -44,6 +43,7 @@ export const preferUseStoreNaming = createRule({ messages: { invalidNaming: 'Store function should follow the naming pattern "{{expected}}"', + renameTo: 'Rename to "{{name}}"', }, }, defaultOptions: [{ prefix: 'use', suffix: 'Store' }], @@ -95,7 +95,8 @@ export const preferUseStoreNaming = createRule({ }, suggest: [ { - desc: `Rename to "${suggestedName}"`, + messageId: 'renameTo', + data: { name: suggestedName }, fix(fixer) { return fixer.replaceText(node.id, suggestedName) }, diff --git a/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts index e7fdabdbe0..0e7ef81655 100644 --- a/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts +++ b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts @@ -8,7 +8,7 @@ import { isDefineStoreCall, isSetupStore, getSetupFunction, - extractDeclarations, + extractTopLevelDeclarations, extractReturnIdentifiers, findReturnStatement, hasSpreadInReturn, @@ -30,7 +30,6 @@ export const requireSetupStorePropertiesExport = createRule({ type: 'problem', docs: { description: 'require all setup store properties to be exported', - recommended: 'error', }, fixable: 'code', schema: [], @@ -55,8 +54,10 @@ export const requireSetupStorePropertiesExport = createRule({ return } - // Extract all declared variables and functions - const { variables, functions } = extractDeclarations(setupFunction.body) + // Extract all declared variables and functions (top-level only) + const { variables, functions } = extractTopLevelDeclarations( + setupFunction.body + ) const allDeclared = [...variables, ...functions] // Find the return statement diff --git a/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts index 9d1353b43c..7d502e81cc 100644 --- a/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts +++ b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts @@ -8,8 +8,9 @@ import type { TSESTree } from '@typescript-eslint/utils' import { isDefineStoreCall, getStoreId, - extractDeclarations, + extractTopLevelDeclarations, extractReturnProperties, + extractReturnIdentifiers, } from '../ast-utils' function parseCode(code: string): TSESTree.Program { @@ -110,8 +111,8 @@ describe('getStoreId', () => { }) }) -describe('extractDeclarations', () => { - it('should extract variable declarations and deduplicate', () => { +describe('extractTopLevelDeclarations', () => { + it('should extract variable declarations and deduplicate (top-level only)', () => { const code = ` function setup() { var name = ref('test') @@ -122,12 +123,12 @@ describe('extractDeclarations', () => { ` const ast = parseCode(code) const func = ast.body[0] as TSESTree.FunctionDeclaration - const result = extractDeclarations(func.body!) + const result = extractTopLevelDeclarations(func.body!) expect(result.variables).toEqual(['name', 'count']) }) - it('should extract loop initializer declarations', () => { + it('should not include loop initializer declarations (top-level only)', () => { const code = ` function setup() { for (let i = 0; i < 10; i++) { @@ -141,13 +142,14 @@ describe('extractDeclarations', () => { ` const ast = parseCode(code) const func = ast.body[0] as TSESTree.FunctionDeclaration - const result = extractDeclarations(func.body!) + const result = extractTopLevelDeclarations(func.body!) - expect(result.variables).toContain('i') - expect(result.variables).toContain('item') + expect(result.variables).not.toContain('i') + expect(result.variables).not.toContain('item') + expect(result.variables).toEqual([]) }) - it('should extract catch clause parameters', () => { + it('should not include catch clause parameters (top-level only)', () => { const code = ` function setup() { try { @@ -160,9 +162,10 @@ describe('extractDeclarations', () => { ` const ast = parseCode(code) const func = ast.body[0] as TSESTree.FunctionDeclaration - const result = extractDeclarations(func.body!) + const result = extractTopLevelDeclarations(func.body!) - expect(result.variables).toContain('error') + expect(result.variables).not.toContain('error') + expect(result.variables).toEqual([]) }) }) @@ -191,7 +194,19 @@ describe('extractReturnProperties', () => { const func = ast.body[0] as TSESTree.FunctionDeclaration const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement const result = extractReturnProperties(returnStmt) + expect(result).toEqual(['name', 'count']) + }) + it('should extract computed string-literal property keys', () => { + const code = ` + function setup() { + return { ["name"]: value, ['count']: value2 } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[0] as TSESTree.ReturnStatement + const result = extractReturnProperties(returnStmt) expect(result).toEqual(['name', 'count']) }) @@ -222,4 +237,19 @@ describe('extractReturnProperties', () => { expect(result).toEqual([]) }) + + it('extractReturnIdentifiers should unwrap TS/JS wrappers to find identifiers', () => { + const code = ` + function setup() { + const count = 1 + const total = 2 + return { a: (count as number), b: ((total)), c: (count!) } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const returnStmt = func.body!.body[2] as TSESTree.ReturnStatement + const result = extractReturnIdentifiers(returnStmt) + expect(result).toEqual(['count', 'total', 'count']) + }) }) diff --git a/packages/eslint-plugin/src/utils/ast-utils.ts b/packages/eslint-plugin/src/utils/ast-utils.ts index bf57dc79a8..9b71b760f8 100644 --- a/packages/eslint-plugin/src/utils/ast-utils.ts +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -30,14 +30,40 @@ function isDefineStoreCallee( return true } - // Member expression: pinia.defineStore(...) + // Member expression: pinia.defineStore(...), pinia['defineStore'](...), pinia[`defineStore`](...) + if (callee.type === 'MemberExpression') { + if ( + !callee.computed && + callee.property.type === 'Identifier' && + callee.property.name === 'defineStore' + ) { + return true + } + if ( + callee.computed && + callee.property.type === 'Literal' && + callee.property.value === 'defineStore' + ) { + return true + } + if ( + callee.computed && + callee.property.type === 'TemplateLiteral' && + callee.property.expressions.length === 0 && + callee.property.quasis[0]?.value.cooked === 'defineStore' + ) { + return true + } + } + + // TS wrappers: unwrap and re-check if ( - callee.type === 'MemberExpression' && - !callee.computed && - callee.property.type === 'Identifier' && - callee.property.name === 'defineStore' + callee.type === 'TSNonNullExpression' || + callee.type === 'TSAsExpression' || + callee.type === 'TSSatisfiesExpression' ) { - return true + // @ts-ignore - these nodes expose `.expression` + return isDefineStoreCallee((callee as any).expression) } // Chain expression (optional chaining): pinia?.defineStore(...) @@ -76,9 +102,16 @@ export function getStoreId(node: TSESTree.CallExpression): string | null { for (const prop of firstArg.properties) { if ( prop.type === 'Property' && - !prop.computed && - prop.key.type === 'Identifier' && - prop.key.name === 'id' + ((!prop.computed && + prop.key.type === 'Identifier' && + prop.key.name === 'id') || + (prop.computed && + prop.key.type === 'Literal' && + prop.key.value === 'id') || + (prop.computed && + prop.key.type === 'TemplateLiteral' && + prop.key.expressions.length === 0 && + prop.key.quasis[0]?.value.cooked === 'id')) ) { // Handle string literal value if ( @@ -229,6 +262,43 @@ export function extractDeclarations(body: TSESTree.BlockStatement): { } } +/** + * Extracts top-level variable and function declarations from a function body + * Only iterates over body.body and collects VariableDeclaration and FunctionDeclaration identifiers + */ +export function extractTopLevelDeclarations(body: TSESTree.BlockStatement): { + variables: string[] + functions: string[] +} { + const variableSet = new Set() + const functionSet = new Set() + + for (const statement of body.body) { + switch (statement.type) { + case 'VariableDeclaration': { + for (const declarator of statement.declarations) { + extractIdentifiersFromPattern(declarator.id, variableSet) + } + break + } + case 'FunctionDeclaration': { + if (statement.id) { + functionSet.add(statement.id.name) + } + break + } + default: + // ignore nested scopes/statements + break + } + } + + return { + variables: Array.from(variableSet), + functions: Array.from(functionSet), + } +} + /** * Extracts identifier names from patterns (handles destructuring) */ @@ -293,6 +363,14 @@ export function extractReturnProperties( ) { properties.push(prop.key.value) } + // Handle computed string literal keys: { ["name"]: ... } or { ['name']: ... } + else if ( + prop.computed && + prop.key.type === 'Literal' && + typeof prop.key.value === 'string' + ) { + properties.push(prop.key.value) + } // Handle computed property keys with template literals: { [`name`]: ... } else if ( prop.computed && @@ -339,14 +417,34 @@ export function extractReturnIdentifiers( const identifiers: string[] = [] + // Helper to unwrap TS/JS wrappers and chain/paren expressions + function unwrap(expr: any): any { + let current = expr + // Unwrap nested wrappers + while ( + current && + (current.type === 'TSAsExpression' || + current.type === 'TSSatisfiesExpression' || + current.type === 'TSNonNullExpression' || + current.type === 'ParenthesizedExpression' || + current.type === 'ChainExpression') + ) { + current = current.expression + } + return current + } + for (const prop of returnStatement.argument.properties) { if (prop.type === 'Property') { if (prop.shorthand && prop.key.type === 'Identifier') { // Shorthand property: { count } -> count identifiers.push(prop.key.name) - } else if (prop.value.type === 'Identifier') { - // Aliased property: { total: count } -> count - identifiers.push(prop.value.name) + } else if ('value' in prop) { + const unwrapped = unwrap((prop as any).value) + if (unwrapped && unwrapped.type === 'Identifier') { + // Aliased property possibly wrapped: { total: (count as number) } -> count + identifiers.push(unwrapped.name) + } } } // Skip spread elements as we can't determine what's being spread @@ -415,6 +513,14 @@ export function findAllReturnStatements( } } break + case 'LabeledStatement': + traverse(node.body) + break + case 'StaticBlock': + for (const statement of node.body) { + traverse(statement) + } + break case 'TryStatement': traverse(node.block) if (node.handler) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b9d4e86f..4f5c39b3cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: packages/eslint-plugin: devDependencies: + '@eslint/js': + specifier: ^9.35.0 + version: 9.35.0 '@typescript-eslint/parser': specifier: ^8.35.1 version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) @@ -148,7 +151,7 @@ importers: specifier: ^8.35.1 version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) eslint: - specifier: ^9.0.0 + specifier: ^9.35.0 version: 9.35.0(jiti@2.4.2) pinia: specifier: workspace:* @@ -159,6 +162,9 @@ importers: typescript: specifier: ~5.8.3 version: 5.8.3 + typescript-eslint: + specifier: ^8.44.0 + version: 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@24.0.8)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.4.2)(terser@5.36.0)(yaml@2.8.0) @@ -1649,6 +1655,14 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.44.0': + resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.43.0': resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1656,11 +1670,12 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.35.1': - resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + '@typescript-eslint/parser@8.44.0': + resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/project-service@8.43.0': resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} @@ -1668,6 +1683,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.44.0': + resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/rule-tester@8.43.0': resolution: {integrity: sha512-DZNnTOjVz9fkZl5Az6h5r0FLfmnw2N2jHLHUluTwKZSs6wZBpIseRBSGmSIoTnye2dmOxagEzFfFQ/OoluIHJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1678,11 +1699,9 @@ packages: resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.35.1': - resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + '@typescript-eslint/scope-manager@8.44.0': + resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/tsconfig-utils@8.43.0': resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} @@ -1690,19 +1709,26 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.35.1': - resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + '@typescript-eslint/tsconfig-utils@8.44.0': + resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.44.0': + resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.43.0': resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.35.1': - resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + '@typescript-eslint/types@8.44.0': + resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/typescript-estree@8.43.0': resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} @@ -1710,6 +1736,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.44.0': + resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.43.0': resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1717,14 +1749,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.35.1': - resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + '@typescript-eslint/utils@8.44.0': + resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/visitor-keys@8.43.0': resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.44.0': + resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -3240,6 +3279,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5011,6 +5053,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -5377,6 +5420,13 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typescript-eslint@8.44.0: + resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.7.2: resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} @@ -7572,6 +7622,23 @@ snapshots: '@types/node': 24.0.8 optional: true + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.44.0 + eslint: 9.35.0(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.43.0 @@ -7584,11 +7651,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + '@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) - '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 + eslint: 9.35.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7602,6 +7672,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.44.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.8.3) + '@typescript-eslint/types': 8.44.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/rule-tester@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) @@ -7621,24 +7700,41 @@ snapshots: '@typescript-eslint/types': 8.43.0 '@typescript-eslint/visitor-keys': 8.43.0 - '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + '@typescript-eslint/scope-manager@8.44.0': dependencies: - typescript: 5.8.3 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/types@8.35.1': {} + '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.35.0(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/types@8.43.0': {} - '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': + '@typescript-eslint/types@8.44.0': {} + + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) - '@typescript-eslint/types': 8.35.1 - '@typescript-eslint/visitor-keys': 8.35.1 + '@typescript-eslint/project-service': 8.43.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -7649,12 +7745,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.43.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/project-service': 8.44.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.8.3) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -7676,16 +7772,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.1': + '@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.35.1 - eslint-visitor-keys: 4.2.1 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3) + eslint: 9.35.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/visitor-keys@8.43.0': dependencies: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.44.0': + dependencies: + '@typescript-eslint/types': 8.44.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.2.0': {} '@unhead/vue@2.0.11(vue@3.5.17(typescript@5.8.3))': @@ -8819,7 +8926,7 @@ snapshots: detective-typescript@14.0.0(typescript@5.8.3): dependencies: - '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) ast-module-types: 6.0.1 node-source-walk: 7.0.1 typescript: 5.8.3 @@ -9429,6 +9536,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -11726,6 +11835,17 @@ snapshots: typescript: 5.8.3 yaml: 2.8.0 + typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.35.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + typescript@5.7.2: {} typescript@5.8.3: {}