Skip to content

Commit 475ea43

Browse files
authored
Merge pull request #23 from andrewmolyuk/dev
Dev
2 parents 5fe4548 + 1973e2d commit 475ea43

File tree

9 files changed

+190
-15
lines changed

9 files changed

+190
-15
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@
3030
"workbench.colorTheme": "Visual Studio Dark",
3131
"workbench.layoutControl.enabled": false,
3232
// Spell check settings
33-
"cSpell.words": ["Codacy", "elif", "Molyuk", "releaserc", "vitest"]
33+
"cSpell.words": ["basenames", "Codacy", "elif", "Molyuk", "releaserc", "vitest"]
3434
}

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ clean:
4040
@git fetch --prune
4141
@git branch -vv | awk '/: gone]/{print $$1}' > .git/branches-to-delete || true
4242
@if [ -s .git/branches-to-delete ]; then \
43-
echo "---" ; cat .git/branches-to-delete ; \
4443
while read b; do \
4544
if [ "$$b" != "$(shell git rev-parse --abbrev-ref HEAD)" ]; then \
4645
git branch -D $$b || true; \

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ This plugin provides rules to enforce modular architecture boundaries in Vue.js
107107

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

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

157157
### Store Rules
158158

159-
| Rule | Description |
160-
| ------------------------- | ------------------------------------------------------------------------------- |
161-
| stores-shared-location | Global state must be in `shared/stores/`. |
162-
| stores-feature-location | Feature-specific state must be in `features/{feature}/stores/`. |
163-
| store-pinia-composition | Store files must use Pinia composition API syntax. |
164-
| store-filename-no-suffix | Store files must not have "Store" suffix (e.g., `auth.ts`, not `authStore.ts`). |
165-
| stores-cross-cutting | Cross-cutting concerns (auth, notifications) must be in `shared/stores/`. |
166-
| feature-stores-no-imports | Feature stores cannot import other feature stores directly. |
159+
| Rule | Description |
160+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
161+
| stores-shared-location | Global state must be in `shared/stores/`. |
162+
| stores-feature-location | Feature-specific state must be in `features/{feature}/stores/`. |
163+
| store-pinia-composition | Store files must use Pinia composition API syntax. |
164+
| [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`). |
165+
| stores-cross-cutting | Cross-cutting concerns (auth, notifications) must be in `shared/stores/`. |
166+
| feature-stores-no-imports | Feature stores cannot import other feature stores directly. |
167167

168168
### Type Rules
169169

docs/rules/service-filename-no-suffix.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,25 @@ src/features/user/services/userService.ts
3434

3535
## Options
3636

37-
The rule accepts a single optional object with these properties:
37+
This rule accepts a single options object with the following properties:
3838

39+
- `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'`).
3940
- `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.
4041

4142
Example configuration:
4243

4344
```js
45+
// Use defaults (forbid 'Service' suffix)
4446
{
4547
"vue-modular/service-filename-no-suffix": ["error"]
4648
}
4749

48-
// With custom ignores
50+
// Change suffix to 'ServiceModule' and ignore legacy files
4951
{
50-
"vue-modular/service-filename-no-suffix": ["error", { "ignores": ["**/legacy/**"] }]
52+
"vue-modular/service-filename-no-suffix": [
53+
"error",
54+
{ "suffix": "ServiceModule", "ignores": ["**/legacy/**"] }
55+
]
5156
}
5257
```
5358

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# vue-modular/store-filename-no-suffix
2+
3+
Disallow the `Store` suffix in store filenames (for example `auth.ts`, not `authStore.ts`).
4+
5+
## Rule details
6+
7+
This rule enforces concise store filenames for files placed under the configured store locations:
8+
9+
- `src/shared/stores/` (cross-cutting stores)
10+
- `src/features/{feature}/stores/` (feature-specific stores)
11+
12+
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`).
13+
14+
Why this rule
15+
16+
- Shorter filenames are easier to scan and keep naming consistent with other resource types (services, composables, types).
17+
- The suffix is redundant because the file location already implies the file is a store.
18+
19+
## Examples
20+
21+
### ✅ Valid
22+
23+
```text
24+
src/shared/stores/auth.ts
25+
src/features/auth/stores/token.ts
26+
```
27+
28+
### ❌ Invalid
29+
30+
```text
31+
src/shared/stores/authStore.ts
32+
src/features/user/stores/userStore.ts
33+
```
34+
35+
## Options
36+
37+
This rule accepts a single options object with the following properties:
38+
39+
- `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'`).
40+
- `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.
41+
42+
Example configuration:
43+
44+
```js
45+
// Use defaults (forbid 'Store' suffix)
46+
{
47+
"vue-modular/store-filename-no-suffix": ["error"]
48+
}
49+
50+
// Change suffix to 'StoreModule' and ignore legacy files
51+
{
52+
"vue-modular/store-filename-no-suffix": [
53+
"error",
54+
{ "suffix": "StoreModule", "ignores": ["**/legacy/**"] }
55+
]
56+
}
57+
```
58+
59+
## Usage notes
60+
61+
- 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.
62+
- 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).
63+
64+
## When not to use
65+
66+
- If your project intentionally includes the `Store` suffix in filenames, do not enable this rule.
67+
- During migrations where many legacy files retain the suffix, prefer adding ignore globs or disabling the rule temporarily.
68+
69+
## Further reading
70+
71+
- See the plugin's architecture guide for store location conventions: `docs/vue-modular-architecture.md`.

src/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { folderKebabCase } from './rules/folder-kebab-case'
77
import { sfcRequired } from './rules/sfc-required'
88
import { sharedImports } from './rules/shared-imports'
99
import { serviceFilenameNoSuffix } from './rules/service-filename-no-suffix'
10+
import { storeFilenameNoSuffix } from './rules/store-filename-no-suffix'
1011
import { sharedUiIndexRequired } from './rules/shared-ui-index-required'
1112
import { VueModularRuleModule } from './types'
1213

@@ -20,5 +21,6 @@ export const rules: Record<string, VueModularRuleModule> = {
2021
'sfc-required': sfcRequired,
2122
'shared-imports': sharedImports,
2223
'service-filename-no-suffix': serviceFilenameNoSuffix,
24+
'store-filename-no-suffix': storeFilenameNoSuffix,
2325
'feature-imports': featureImports,
2426
}

src/rules/service-filename-no-suffix.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnor
33
import type { VueModularRuleModule, VueModularRuleContext } from '../types'
44

55
const defaultOptions = {
6+
suffix: 'Service',
67
ignores: ['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*'],
78
}
89

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

24-
if (/Service$/i.test(base)) {
25+
if (new RegExp(`${options.suffix}$`, 'i').test(base)) {
2526
context.report({
2627
node,
2728
messageId: 'noServiceSuffix',
@@ -45,6 +46,7 @@ export const serviceFilenameNoSuffix = createRule<VueModularRuleModule>({
4546
{
4647
type: 'object',
4748
properties: {
49+
suffix: { type: 'string' },
4850
ignores: { type: 'array', items: { type: 'string' } },
4951
},
5052
additionalProperties: false,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import path from 'path'
2+
import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnored, isTs } from '../utils'
3+
import type { VueModularRuleModule, VueModularRuleContext } from '../types'
4+
5+
const defaultOptions = {
6+
suffix: 'Store',
7+
ignores: ['**/*.d.ts', '**/*.spec.*', '**/*.test.*', '**/*.stories.*'],
8+
}
9+
10+
export const storeFilenameNoSuffix = createRule<VueModularRuleModule>({
11+
create(context: VueModularRuleContext) {
12+
const options = parseRuleOptions(context, defaultOptions)
13+
const projectOptions = parseProjectOptions(context)
14+
const filename = resolvePath(context.filename, projectOptions.rootPath, projectOptions.rootAlias)
15+
if (!filename) return {}
16+
if (isIgnored(filename, options.ignores)) return {}
17+
18+
// Only check TypeScript files
19+
if (!isTs(context.filename)) return {}
20+
21+
// Only check files inside shared/stores or features/*/stores
22+
const inSharedStores = filename.startsWith(`${projectOptions.sharedPath}/stores`)
23+
const inFeatureStores = filename.startsWith(projectOptions.featuresPath) && filename.includes('/stores/')
24+
if (!inSharedStores && !inFeatureStores) return {}
25+
26+
return {
27+
Program(node) {
28+
const base = path.basename(context.filename, path.extname(context.filename))
29+
30+
if (new RegExp(`${options.suffix}$`, 'i').test(base)) {
31+
context.report({
32+
node,
33+
messageId: 'noStoreSuffix',
34+
data: { filename: path.basename(context.filename) },
35+
})
36+
}
37+
},
38+
}
39+
},
40+
name: 'store-filename-no-suffix',
41+
recommended: true,
42+
level: 'error',
43+
meta: {
44+
type: 'suggestion',
45+
docs: {
46+
category: 'Store Rules',
47+
description: 'Store files must not have "Store" suffix (e.g., auth.ts, not authStore.ts)',
48+
},
49+
defaultOptions: [defaultOptions],
50+
schema: [
51+
{
52+
type: 'object',
53+
properties: {
54+
suffix: { type: 'string' },
55+
ignores: { type: 'array', items: { type: 'string' } },
56+
},
57+
additionalProperties: false,
58+
},
59+
],
60+
messages: {
61+
noStoreSuffix: 'Store filename "{{filename}}" should not include the "Store" suffix (use e.g. "auth.ts").',
62+
},
63+
},
64+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, it } from 'vitest'
2+
import { storeFilenameNoSuffix } from '../../src/rules/store-filename-no-suffix'
3+
import { getRuleTester } from '../test-utils'
4+
5+
const ruleTester = getRuleTester()
6+
7+
describe('store-filename-no-suffix', () => {
8+
it('flags files with Store suffix inside stores directories', () => {
9+
ruleTester.run('store-filename-no-suffix', storeFilenameNoSuffix, {
10+
valid: [
11+
{ code: '// ok', filename: 'src/shared/stores/auth.ts' },
12+
{ code: '// ok', filename: 'src/features/auth/useAuth.ts' },
13+
{ code: '// ok', filename: 'src/features/auth/stores/tokenStore.ts', options: [{ ignores: ['**/tokenStore.ts'] }] },
14+
{ code: '// ok', filename: 'src/shared/stores/authStore.js' },
15+
{ code: '// ok', filename: 'authStore.ts' },
16+
{ code: '// ok', filename: 'src/shared/stores/authStore.test.ts' },
17+
],
18+
invalid: [
19+
{
20+
code: '// bad',
21+
filename: 'src/shared/stores/authStore.ts',
22+
errors: [{ messageId: 'noStoreSuffix', data: { filename: 'authStore.ts' } }],
23+
},
24+
{
25+
code: '// bad',
26+
filename: 'src/features/user/stores/userStore.ts',
27+
errors: [{ messageId: 'noStoreSuffix', data: { filename: 'userStore.ts' } }],
28+
},
29+
],
30+
})
31+
})
32+
})

0 commit comments

Comments
 (0)