diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 0000000000..933b54dfab --- /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 + 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..cc7b9d305b --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -0,0 +1,73 @@ +{ + "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.cjs", + "import": "./dist/index.mjs" + }, + "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", + "dist/*.map" + ], + "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.35.0", + "pinia": "workspace:*", + "tsup": "^8.5.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.44.0", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "eslint": ">=8.57.0 || ^9.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..34516b45ea --- /dev/null +++ b/packages/eslint-plugin/src/index.ts @@ -0,0 +1,47 @@ +/** + * @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' +import packageJson from '../package.json' + +/** + * 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: packageJson.version, + }, + rules: { + 'require-setup-store-properties-export': requireSetupStorePropertiesExport, + 'no-circular-store-dependencies': noCircularStoreDependencies, + 'prefer-use-store-naming': preferUseStoreNaming, + 'no-store-in-computed': noStoreInComputed, + }, +} + +// 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', + '@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..70610613f6 --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/no-circular-store-dependencies.test.ts @@ -0,0 +1,125 @@ +/** + * @fileoverview Tests for no-circular-store-dependencies rule + */ + +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, + 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..2d4f2b743d --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/no-store-in-computed.test.ts @@ -0,0 +1,128 @@ +/** + * @fileoverview Tests for no-store-in-computed rule + */ + +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, + 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..02b2baef6c --- /dev/null +++ b/packages/eslint-plugin/src/rules/__tests__/prefer-use-store-naming.test.ts @@ -0,0 +1,183 @@ +/** + * @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', + suggestions: [ + { + desc: 'Rename to "useDataStore"', + output: `export const useDataStore = defineStore('data', () => {})`, + }, + ], + }, + ], + }, + // Missing 'Store' suffix + { + code: ` + export const useUser = defineStore('user', () => { + const name = ref('John') + return { name } + }) + `, + errors: [ + { + messageId: 'invalidNaming', + suggestions: [ + { + desc: 'Rename to "useUserStore"', + 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', + suggestions: [ + { + desc: 'Rename to "useUserStore"', + 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', + suggestions: [ + { + desc: 'Rename to "useShoppingCartStore"', + 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', + 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/__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..6ded8f13de --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-circular-store-dependencies.ts @@ -0,0 +1,215 @@ +/** + * @fileoverview Rule to detect circular dependencies between stores + * @author Eduardo San Martin Morote + */ + +import { + ESLintUtils, + type TSESTree, + type TSESLint, +} 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', + }, + schema: [], + messages: { + circularDependency: + 'Potential circular dependency detected: store "{{currentStore}}" uses "{{usedStore}}"', + setupCircularDependency: + 'Avoid using other stores directly in setup function body. Use them in actions instead.', + }, + }, + defaultOptions: [], + create(context) { + const storeUsages = new Map() // currentStore -> [usedStores] + const usageGraph = new Map>() // currentStore -> usedStore -> nodes + const storeStack: string[] = [] + + 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' + ) { + 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, context) + } + } + } + } + + // Track store usage calls + const currentStoreName = storeStack[storeStack.length - 1] + 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) + } + // 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) || [] + if (usedStoreUsages.includes(currentStoreName)) { + context.report({ + node, + messageId: 'circularDependency', + data: { + currentStore: currentStoreName, + usedStore: usedStoreName, + }, + }) + } + } + } + }, + + 'CallExpression:exit'(node: TSESTree.CallExpression) { + if (isDefineStoreCall(node)) { + storeStack.pop() + } + }, + + 'Program:exit'() { + // Check for indirect circular dependencies + checkIndirectCircularDependencies(usageGraph, context) + }, + } + }, +}) + +/** + * Checks setup function for direct store usage in the function body + */ +function checkSetupFunctionForStoreUsage( + setupFunction: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + context: TSESLint.RuleContext< + 'circularDependency' | 'setupCircularDependency', + [] + > +) { + 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( + usageGraph: Map>, + context: TSESLint.RuleContext< + 'circularDependency' | 'setupCircularDependency', + [] + > +) { + const visited = new Set() + const inPath = new Set() + const path: string[] = [] + const reported = new Set() // "A->B" + + const dfs = (store: string) => { + visited.add(store) + inPath.add(store) + path.push(store) + const deps = usageGraph.get(store) ?? new Map() + for (const [dep] of deps) { + 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 }, + }) + } + } + } else if (!visited.has(dep)) { + dfs(dep) + } + } + path.pop() + inPath.delete(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 new file mode 100644 index 0000000000..1c0798828f --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-store-in-computed.ts @@ -0,0 +1,129 @@ +/** + * @fileoverview Rule to prevent store instantiation in computed properties + * @author Eduardo San Martin Morote + */ + +import { + ESLintUtils, + type TSESTree, + type TSESLint, +} 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', + }, + schema: [], + messages: { + noStoreInComputed: + 'Avoid instantiating stores inside computed properties. Move store instantiation to the top level.', + }, + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node: TSESTree.CallExpression) { + 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: TSESLint.RuleContext<'noStoreInComputed', []> +) { + 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..448f4660f2 --- /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, getStoreId } 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', + }, + hasSuggestions: true, + 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 "{{expected}}"', + renameTo: 'Rename to "{{name}}"', + }, + }, + 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 + const storeId = getStoreId(node.init) + if (storeId) { + // 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: { + expected: suggestedName, + }, + suggest: [ + { + 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 new file mode 100644 index 0000000000..0e7ef81655 --- /dev/null +++ b/packages/eslint-plugin/src/rules/require-setup-store-properties-export.ts @@ -0,0 +1,139 @@ +/** + * @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, + extractTopLevelDeclarations, + extractReturnIdentifiers, + findReturnStatement, + hasSpreadInReturn, +} 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', + }, + 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 (top-level only) + const { variables, functions } = extractTopLevelDeclarations( + 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 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) => !exportedIdentifiers.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/__tests__/ast-utils.test.ts b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts new file mode 100644 index 0000000000..7d502e81cc --- /dev/null +++ b/packages/eslint-plugin/src/utils/__tests__/ast-utils.test.ts @@ -0,0 +1,255 @@ +/** + * @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, + extractTopLevelDeclarations, + extractReturnProperties, + extractReturnIdentifiers, +} 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('extractTopLevelDeclarations', () => { + it('should extract variable declarations and deduplicate (top-level only)', () => { + const code = ` + function setup() { + var name = ref('test') + let count = 0 + var name = ref('duplicate') // This should be deduplicated + return { name, count } + } + ` + const ast = parseCode(code) + const func = ast.body[0] as TSESTree.FunctionDeclaration + const result = extractTopLevelDeclarations(func.body!) + + expect(result.variables).toEqual(['name', 'count']) + }) + + it('should not include loop initializer declarations (top-level only)', () => { + 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 = extractTopLevelDeclarations(func.body!) + + expect(result.variables).not.toContain('i') + expect(result.variables).not.toContain('item') + expect(result.variables).toEqual([]) + }) + + it('should not include catch clause parameters (top-level only)', () => { + 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 = extractTopLevelDeclarations(func.body!) + + expect(result.variables).not.toContain('error') + expect(result.variables).toEqual([]) + }) +}) + +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 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']) + }) + + 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([]) + }) + + 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 new file mode 100644 index 0000000000..9b71b760f8 --- /dev/null +++ b/packages/eslint-plugin/src/utils/ast-utils.ts @@ -0,0 +1,581 @@ +/** + * @fileoverview AST utilities for Pinia ESLint plugin + */ + +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 { + 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(...), 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 === 'TSNonNullExpression' || + callee.type === 'TSAsExpression' || + callee.type === 'TSSatisfiesExpression' + ) { + // @ts-ignore - these nodes expose `.expression` + return isDefineStoreCallee((callee as any).expression) + } + + // 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.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 ( + 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 + } + } + } + } + + return null +} + +/** + * 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 (recursive) + * Captures loop-initializer declarations and de-duplicates outputs + */ +export function extractDeclarations(body: TSESTree.BlockStatement): { + variables: string[] + 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, variableSet) + } + break + case 'FunctionDeclaration': + if (node.id) { + functionSet.add(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': + // 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) + } + 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) { + // Handle catch clause parameter (e.g., catch (error)) + if (node.handler.param) { + extractIdentifiersFromPattern(node.handler.param, variableSet) + } + 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: Array.from(variableSet), + functions: Array.from(functionSet), + } +} + +/** + * 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) + */ +function extractIdentifiersFromPattern( + pattern: TSESTree.BindingName, + identifiers: Set +): void { + switch (pattern.type) { + case 'Identifier': + identifiers.add(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) + * Handles quoted keys, literal property keys, and MemberExpression returns + */ +export function extractReturnProperties( + returnStatement: TSESTree.ReturnStatement +): string[] { + if (!returnStatement.argument) { + return [] + } + + // Handle object expression returns + if (returnStatement.argument.type === 'ObjectExpression') { + const properties: string[] = [] + + 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 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 && + 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 [] +} + +/** + * 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[] = [] + + // 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 ('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 + } + + 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 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 'LabeledStatement': + traverse(node.body) + break + case 'StaticBlock': + for (const statement of node.body) { + 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 { + 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 + } + } + + // If no object return found, return the last return statement + return allReturns[allReturns.length - 1] +} 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/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..98a7df973b --- /dev/null +++ b/packages/eslint-plugin/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + external: ['eslint', '@typescript-eslint/utils'], + target: 'node18', + splitting: false, + sourcemap: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.cjs', + }), +}) 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..4f5c39b3cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,39 @@ 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: + '@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) + '@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.35.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 + 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) + packages/nuxt: dependencies: '@nuxt/kit': @@ -153,7 +186,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 +862,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 +1597,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,30 +1655,113 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/project-service@8.35.1': - resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + '@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: '>=4.8.4 <5.9.0' + '@typescript-eslint/parser': ^8.44.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.35.1': - resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + '@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: - typescript: '>=4.8.4 <5.9.0' + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.35.1': - resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + '@typescript-eslint/parser@8.44.0': + resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} 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/typescript-estree@8.35.1': - resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + '@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 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.35.1': - resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + '@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} + 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/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} + + '@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/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/types@8.44.0': + resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.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/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} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@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': @@ -1889,6 +2062,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 +2095,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 +2284,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 +2313,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 +2681,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 +2915,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 +2928,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 +3033,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 +3070,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 +3104,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 +3115,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 +3238,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 +3259,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'} @@ -3027,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} @@ -3123,6 +3378,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 +3392,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 +3624,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 +3677,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 +3717,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 +3767,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 +3796,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 +3969,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 +4076,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 +4247,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 +4267,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 +4283,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 +4324,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 +4670,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 +4837,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'} @@ -4733,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==} @@ -5058,6 +5379,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'} @@ -5095,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'} @@ -5675,6 +6007,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 +6087,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 +6640,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 +6692,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 +7125,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 +7158,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 +7568,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,27 +7622,135 @@ snapshots: '@types/node': 24.0.8 optional: true - '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + '@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 + '@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/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@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 + + '@typescript-eslint/project-service@8.43.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) - '@typescript-eslint/types': 8.35.1 + '@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/tsconfig-utils@8.35.1(typescript@5.8.3)': + '@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) + '@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/scope-manager@8.44.0': + dependencies: + '@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/typescript-estree@8.35.1(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/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/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/types@8.44.0': {} + + '@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/typescript-estree@8.44.0(typescript@5.8.3)': + dependencies: + '@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 @@ -7256,9 +7761,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.1': + '@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.35.1 + '@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/utils@8.44.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.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': {} @@ -7379,7 +7911,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 +8210,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 +8228,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 +8446,8 @@ snapshots: callsite@1.0.0: {} + callsites@3.1.0: {} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -7932,6 +8477,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 +8861,8 @@ snapshots: deep-eql@5.0.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.0: {} @@ -8374,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 @@ -8561,6 +9113,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 +9125,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 +9272,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 +9303,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 +9334,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 +9351,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 +9489,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 +9517,8 @@ snapshots: globals@11.12.0: {} + globals@14.0.0: {} + globby@14.1.0: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -8895,6 +9536,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -9008,12 +9651,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 +9854,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 +9895,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 +9928,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 +10014,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 +10036,8 @@ snapshots: lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} lodash.uniq@4.5.0: {} @@ -9543,6 +10218,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 +10311,8 @@ snapshots: nanotar@0.2.0: {} + natural-compare@1.4.0: {} + neo-async@2.6.2: {} netlify@13.3.5: @@ -9822,7 +10503,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 +10511,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 +10683,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 +10723,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 +10739,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 +10767,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 +11082,8 @@ snapshots: transitivePeerDependencies: - supports-color + prelude-ls@1.2.1: {} + prettier@3.6.2: {} pretty-bytes@6.1.1: {} @@ -10539,6 +11243,8 @@ snapshots: require-package-name@2.0.1: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve@1.22.10: @@ -11098,6 +11804,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: {} @@ -11125,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: {} @@ -11394,7 +12115,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 +12128,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 +12375,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 +12535,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 +12607,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: {}