Skip to content
Merged

Dev #23

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@
"workbench.colorTheme": "Visual Studio Dark",
"workbench.layoutControl.enabled": false,
// Spell check settings
"cSpell.words": ["Codacy", "elif", "Molyuk", "releaserc", "vitest"]
"cSpell.words": ["basenames", "Codacy", "elif", "Molyuk", "releaserc", "vitest"]
}
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ clean:
@git fetch --prune
@git branch -vv | awk '/: gone]/{print $$1}' > .git/branches-to-delete || true
@if [ -s .git/branches-to-delete ]; then \
echo "---" ; cat .git/branches-to-delete ; \
while read b; do \
if [ "$$b" != "$(shell git rev-parse --abbrev-ref HEAD)" ]; then \
git branch -D $$b || true; \
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ This plugin provides rules to enforce modular architecture boundaries in Vue.js

> The list of rules is a work in progress. Implemented rules are linked below; unimplemented rules are listed as plain names.
>
> ![Progress](https://progress-bar.xyz/11/?scale=92&width=500&title=11%20of%2087%20rules%20completed)
> ![Progress](https://progress-bar.xyz/12/?scale=87&width=500&title=12%20of%2087%20rules%20completed)

### File Organization Rules

Expand Down Expand Up @@ -156,14 +156,14 @@ This plugin provides rules to enforce modular architecture boundaries in Vue.js

### Store Rules

| Rule | Description |
| ------------------------- | ------------------------------------------------------------------------------- |
| stores-shared-location | Global state must be in `shared/stores/`. |
| stores-feature-location | Feature-specific state must be in `features/{feature}/stores/`. |
| store-pinia-composition | Store files must use Pinia composition API syntax. |
| store-filename-no-suffix | Store files must not have "Store" suffix (e.g., `auth.ts`, not `authStore.ts`). |
| stores-cross-cutting | Cross-cutting concerns (auth, notifications) must be in `shared/stores/`. |
| feature-stores-no-imports | Feature stores cannot import other feature stores directly. |
| Rule | Description |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| stores-shared-location | Global state must be in `shared/stores/`. |
| stores-feature-location | Feature-specific state must be in `features/{feature}/stores/`. |
| store-pinia-composition | Store files must use Pinia composition API syntax. |
| [store-filename-no-suffix](./docs/rules/store-filename-no-suffix.md) | Store files must not have "Store" suffix (e.g., `auth.ts`, not `authStore.ts`). |
| stores-cross-cutting | Cross-cutting concerns (auth, notifications) must be in `shared/stores/`. |
| feature-stores-no-imports | Feature stores cannot import other feature stores directly. |

### Type Rules

Expand Down
11 changes: 8 additions & 3 deletions docs/rules/service-filename-no-suffix.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,25 @@ src/features/user/services/userService.ts

## Options

The rule accepts a single optional object with these properties:
This rule accepts a single options object with the following properties:

- `suffix` (string, default: `'Service'`) — the suffix to forbid on filenames. The rule constructs a case-insensitive regular expression from this value and tests file basenames. Use this to forbid a different suffix (for example `'ServiceImpl'`).
- `ignores` (string[], default: `['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*']`) — an array of glob patterns (minimatch) matched against the filename supplied by ESLint. Matching files are skipped.

Example configuration:

```js
// Use defaults (forbid 'Service' suffix)
{
"vue-modular/service-filename-no-suffix": ["error"]
}

// With custom ignores
// Change suffix to 'ServiceModule' and ignore legacy files
{
"vue-modular/service-filename-no-suffix": ["error", { "ignores": ["**/legacy/**"] }]
"vue-modular/service-filename-no-suffix": [
"error",
{ "suffix": "ServiceModule", "ignores": ["**/legacy/**"] }
]
}
```

Expand Down
71 changes: 71 additions & 0 deletions docs/rules/store-filename-no-suffix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# vue-modular/store-filename-no-suffix

Disallow the `Store` suffix in store filenames (for example `auth.ts`, not `authStore.ts`).

## Rule details

This rule enforces concise store filenames for files placed under the configured store locations:

- `src/shared/stores/` (cross-cutting stores)
- `src/features/{feature}/stores/` (feature-specific stores)

The rule only runs for TypeScript files. It reports when a file's basename ends with the `Store` suffix (case-insensitive) and suggests using a shorter name (for example `auth.ts`).

Why this rule

- Shorter filenames are easier to scan and keep naming consistent with other resource types (services, composables, types).
- The suffix is redundant because the file location already implies the file is a store.

## Examples

### ✅ Valid

```text
src/shared/stores/auth.ts
src/features/auth/stores/token.ts
```

### ❌ Invalid

```text
src/shared/stores/authStore.ts
src/features/user/stores/userStore.ts
```

## Options

This rule accepts a single options object with the following properties:

- `suffix` (string, default: `'Store'`) — the suffix to forbid on filenames. The rule constructs a case-insensitive regular expression from this value and tests file basenames. Use this to forbid a different suffix (for example `'StoreModule'`).
- `ignores` (string[], default: `['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*']`) — an array of glob patterns (minimatch) matched against the filename supplied by ESLint. Matching files are skipped.

Example configuration:

```js
// Use defaults (forbid 'Store' suffix)
{
"vue-modular/store-filename-no-suffix": ["error"]
}

// Change suffix to 'StoreModule' and ignore legacy files
{
"vue-modular/store-filename-no-suffix": [
"error",
{ "suffix": "StoreModule", "ignores": ["**/legacy/**"] }
]
}
```

## Usage notes

- The rule uses project settings for `rootPath`, `rootAlias`, `sharedPath`, and `featuresPath` to determine the file's normalized path. These settings are merged with plugin defaults and can be configured via `settings['vue-modular']` in your ESLint configuration.
- Ignore globs use `minimatch` semantics and should be written relative to the filename ESLint receives (which may be absolute or repo-relative depending on your lint invocation).

## When not to use

- If your project intentionally includes the `Store` suffix in filenames, do not enable this rule.
- During migrations where many legacy files retain the suffix, prefer adding ignore globs or disabling the rule temporarily.

## Further reading

- See the plugin's architecture guide for store location conventions: `docs/vue-modular-architecture.md`.
2 changes: 2 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { folderKebabCase } from './rules/folder-kebab-case'
import { sfcRequired } from './rules/sfc-required'
import { sharedImports } from './rules/shared-imports'
import { serviceFilenameNoSuffix } from './rules/service-filename-no-suffix'
import { storeFilenameNoSuffix } from './rules/store-filename-no-suffix'
import { sharedUiIndexRequired } from './rules/shared-ui-index-required'
import { VueModularRuleModule } from './types'

Expand All @@ -20,5 +21,6 @@ export const rules: Record<string, VueModularRuleModule> = {
'sfc-required': sfcRequired,
'shared-imports': sharedImports,
'service-filename-no-suffix': serviceFilenameNoSuffix,
'store-filename-no-suffix': storeFilenameNoSuffix,
'feature-imports': featureImports,
}
4 changes: 3 additions & 1 deletion src/rules/service-filename-no-suffix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnor
import type { VueModularRuleModule, VueModularRuleContext } from '../types'

const defaultOptions = {
suffix: 'Service',
ignores: ['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*'],
}

Expand All @@ -21,7 +22,7 @@ export const serviceFilenameNoSuffix = createRule<VueModularRuleModule>({
Program(node) {
const base = path.basename(context.filename, path.extname(context.filename))

if (/Service$/i.test(base)) {
if (new RegExp(`${options.suffix}$`, 'i').test(base)) {
context.report({
node,
messageId: 'noServiceSuffix',
Expand All @@ -45,6 +46,7 @@ export const serviceFilenameNoSuffix = createRule<VueModularRuleModule>({
{
type: 'object',
properties: {
suffix: { type: 'string' },
ignores: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
Expand Down
64 changes: 64 additions & 0 deletions src/rules/store-filename-no-suffix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import path from 'path'
import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnored, isTs } from '../utils'
import type { VueModularRuleModule, VueModularRuleContext } from '../types'

const defaultOptions = {
suffix: 'Store',
ignores: ['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*'],
}

export const storeFilenameNoSuffix = createRule<VueModularRuleModule>({
create(context: VueModularRuleContext) {
const options = parseRuleOptions(context, defaultOptions)
const projectOptions = parseProjectOptions(context)
const filename = resolvePath(context.filename, projectOptions.rootPath, projectOptions.rootAlias)
if (!filename) return {}
if (isIgnored(filename, options.ignores)) return {}

// Only check TypeScript files
if (!isTs(context.filename)) return {}

// Only check files inside shared/stores or features/*/stores
const inSharedStores = filename.startsWith(`${projectOptions.sharedPath}/stores`)
const inFeatureStores = filename.startsWith(projectOptions.featuresPath) && filename.includes('/stores/')
if (!inSharedStores && !inFeatureStores) return {}

return {
Program(node) {
const base = path.basename(context.filename, path.extname(context.filename))

if (new RegExp(`${options.suffix}$`, 'i').test(base)) {
context.report({
node,
messageId: 'noStoreSuffix',
data: { filename: path.basename(context.filename) },
})
}
},
}
},
name: 'store-filename-no-suffix',
recommended: true,
level: 'error',
meta: {
type: 'suggestion',
docs: {
category: 'Store Rules',
description: 'Store files must not have "Store" suffix (e.g., auth.ts, not authStore.ts)',
},
defaultOptions: [defaultOptions],
schema: [
{
type: 'object',
properties: {
suffix: { type: 'string' },
ignores: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
],
messages: {
noStoreSuffix: 'Store filename "{{filename}}" should not include the "Store" suffix (use e.g. "auth.ts").',
},
},
})
32 changes: 32 additions & 0 deletions tests/rules/store-filename-no-suffix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it } from 'vitest'
import { storeFilenameNoSuffix } from '../../src/rules/store-filename-no-suffix'
import { getRuleTester } from '../test-utils'

const ruleTester = getRuleTester()

describe('store-filename-no-suffix', () => {
it('flags files with Store suffix inside stores directories', () => {
ruleTester.run('store-filename-no-suffix', storeFilenameNoSuffix, {
valid: [
{ code: '// ok', filename: 'src/shared/stores/auth.ts' },
{ code: '// ok', filename: 'src/features/auth/useAuth.ts' },
{ code: '// ok', filename: 'src/features/auth/stores/tokenStore.ts', options: [{ ignores: ['**/tokenStore.ts'] }] },
{ code: '// ok', filename: 'src/shared/stores/authStore.js' },
{ code: '// ok', filename: 'authStore.ts' },
{ code: '// ok', filename: 'src/shared/stores/authStore.test.ts' },
],
invalid: [
{
code: '// bad',
filename: 'src/shared/stores/authStore.ts',
errors: [{ messageId: 'noStoreSuffix', data: { filename: 'authStore.ts' } }],
},
{
code: '// bad',
filename: 'src/features/user/stores/userStore.ts',
errors: [{ messageId: 'noStoreSuffix', data: { filename: 'userStore.ts' } }],
},
],
})
})
})