Skip to content

Commit a3d2fd8

Browse files
committed
feat: add file-component-naming rule to enforce PascalCase for Vue component filenames
1 parent 5ebcd6f commit a3d2fd8

File tree

9 files changed

+331
-102
lines changed

9 files changed

+331
-102
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ This plugin provides rules to enforce modular architecture boundaries in Vue.js
8787
8888
### File Organization Rules
8989

90-
| Rule | Description |
91-
| ------------------------------------- | ---------------------------------------------------------------------------------------- |
92-
| vue-modular/file-component-naming | All Vue components must use PascalCase naming (e.g., `UserForm.vue`, `ProductList.vue`). |
93-
| vue-modular/file-ts-naming | All TypeScript files must use camelCase naming (e.g., `useAuth.ts`, `userApi.ts`). |
94-
| vue-modular/folder-kebab-case | All folders must use kebab-case naming (e.g., `user-management/`, `auth/`). |
95-
| vue-modular/feature-index-required | Each feature folder must contain an `index.ts` file as its public API. |
96-
| vue-modular/components-index-required | All `components/` folders must contain an `index.ts` file for component exports. |
97-
| vue-modular/shared-ui-index-required | The `shared/ui/` folder must contain an `index.ts` file for UI component exports. |
90+
| Rule | Description |
91+
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
92+
| [vue-modular/file-component-naming](./docs/rules/file-component-naming.md) | All Vue components must use PascalCase naming (e.g., `UserForm.vue`, `ProductList.vue`). |
93+
| vue-modular/file-ts-naming | All TypeScript files must use camelCase naming (e.g., `useAuth.ts`, `userApi.ts`). |
94+
| vue-modular/folder-kebab-case | All folders must use kebab-case naming (e.g., `user-management/`, `auth/`). |
95+
| vue-modular/feature-index-required | Each feature folder must contain an `index.ts` file as its public API. |
96+
| vue-modular/components-index-required | All `components/` folders must contain an `index.ts` file for component exports. |
97+
| vue-modular/shared-ui-index-required | The `shared/ui/` folder must contain an `index.ts` file for UI component exports. |
9898

9999
### Dependency Rules
100100

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# vue-modular/file-component-naming
2+
3+
Enforce PascalCase filenames for Vue Single File Components (.vue).
4+
5+
## Rule Details
6+
7+
This rule requires that component filenames use PascalCase (for example `MyButton.vue`) to make component names predictable and consistent across a project.
8+
9+
By default the rule only applies to files under the `src` directory. It supports an `ignore` option with glob patterns to skip files or folders.
10+
11+
Examples of incorrect and correct code for this rule are below.
12+
13+
### Incorrect
14+
15+
```vue
16+
<!-- File: src/components/user-card.vue -->
17+
<!-- Filename should be PascalCase -->
18+
<script setup>
19+
// component implementation
20+
</script>
21+
```
22+
23+
```vue
24+
<!-- File: src/shared/ui/cgs-icon.vue -->
25+
<script>
26+
export default {}
27+
</script>
28+
```
29+
30+
### Correct
31+
32+
```vue
33+
<!-- File: src/components/UserCard.vue -->
34+
<script setup>
35+
// component implementation
36+
</script>
37+
```
38+
39+
```vue
40+
<!-- File: src/shared/ui/CgsIcon.vue -->
41+
<script>
42+
export default {}
43+
</script>
44+
```
45+
46+
## Options
47+
48+
This rule accepts an object with the following properties:
49+
50+
- `src` (string, default: `"src"`) - base source directory to scope the rule. When set, files outside this directory are ignored.
51+
- `ignore` (string[], default: `[]`) - array of glob patterns (minimatch) to ignore. Patterns are matched against both the absolute file path and the project-relative path.
52+
53+
Examples:
54+
55+
```js
56+
// Use defaults (apply inside `src` only)
57+
{
58+
"vue-modular/file-component-naming": ["error"]
59+
}
60+
61+
// Custom source directory and ignored patterns
62+
{
63+
"vue-modular/file-component-naming": ["error", { "src": "app", "ignore": ["**/tests/**", "**/*.spec.vue"] }]
64+
}
65+
```
66+
67+
## Usage Notes
68+
69+
- The rule only lints `.vue` files.
70+
- The `src` option is applied by checking whether the file path contains the configured segment (for example `src` or `app`). If the segment is not present the file is ignored.
71+
- The `ignore` patterns use `minimatch` semantics and are matched against the absolute filename and the path relative to the repository root.
72+
73+
## When Not To Use
74+
75+
- If your project convention requires a different casing (for example `kebab-case` filenames) you should not enable this rule.
76+
- If your build tooling rewrites or normalizes component filenames automatically, this rule may produce false positives.
77+
78+
## Further Reading
79+
80+
- Vue style guide: <https://vuejs.org/style-guide/>

src/configs.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const recommendedRules = {
2+
'vue-modular/no-cross-feature-imports': 'error',
3+
'vue-modular/no-cross-module-imports': 'error',
4+
'vue-modular/no-business-logic-in-ui-kit': 'error',
5+
'vue-modular/enforce-import-boundaries': 'error',
6+
'vue-modular/enforce-src-structure': 'error',
7+
'vue-modular/enforce-app-structure': 'error',
8+
'vue-modular/enforce-module-exports': 'error',
9+
'vue-modular/enforce-feature-exports': 'error',
10+
'vue-modular/no-orphaned-files': 'error',
11+
'vue-modular/enforce-sfc-order': 'error',
12+
}
13+
14+
const allRules = {
15+
...recommendedRules,
16+
'vue-modular/enforce-naming-convention': 'error',
17+
'vue-modular/file-component-naming': 'error',
18+
'vue-modular/no-deep-nesting': 'error',
19+
}
20+
21+
const createConfigs = (plugin) => {
22+
const flatPluginBlock = {
23+
plugins: {
24+
'vue-modular': plugin,
25+
},
26+
languageOptions: {
27+
ecmaVersion: 2022,
28+
sourceType: 'module',
29+
},
30+
}
31+
32+
const legacyPluginBlock = {
33+
plugins: ['vue-modular'],
34+
}
35+
36+
return {
37+
'flat/recommended': [
38+
{
39+
...flatPluginBlock,
40+
rules: recommendedRules,
41+
},
42+
],
43+
44+
'flat/strict': [
45+
{
46+
...flatPluginBlock,
47+
rules: allRules,
48+
},
49+
],
50+
51+
recommended: {
52+
...legacyPluginBlock,
53+
rules: recommendedRules,
54+
},
55+
56+
strict: {
57+
...legacyPluginBlock,
58+
rules: allRules,
59+
},
60+
}
61+
}
62+
63+
export default createConfigs

src/index.js

Lines changed: 5 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import enforceModuleExports from './rules/enforce-module-exports.js'
88
import enforceFeatureExports from './rules/enforce-feature-exports.js'
99
import enforceImportBoundaries from './rules/enforce-import-boundaries.js'
1010
import enforceNamingConvention from './rules/enforce-naming-convention.js'
11+
import fileComponentNaming from './rules/file-component-naming.js'
1112
import noBusinessLogicInUiKit from './rules/no-business-logic-in-ui-kit.js'
1213
import noOrphanedFiles from './rules/no-orphaned-files.js'
1314
import noDeepNesting from './rules/no-deep-nesting.js'
1415
import enforceSfcOrder from './rules/enforce-sfc-order.js'
15-
16-
// Import utilities
17-
import { isTestFile } from './utils/import-boundaries.js'
16+
import createConfigs from './configs.js'
1817

1918
const plugin = {
2019
meta,
@@ -26,6 +25,7 @@ const plugin = {
2625
'enforce-module-exports': enforceModuleExports,
2726
'enforce-feature-exports': enforceFeatureExports,
2827
'enforce-naming-convention': enforceNamingConvention,
28+
'file-component-naming': fileComponentNaming,
2929
'enforce-import-boundaries': enforceImportBoundaries,
3030
'no-business-logic-in-ui-kit': noBusinessLogicInUiKit,
3131
'no-orphaned-files': noOrphanedFiles,
@@ -34,98 +34,9 @@ const plugin = {
3434
},
3535
processors: {},
3636
configs: {},
37-
utils: {
38-
isTestFile,
39-
},
37+
utils: {},
4038
}
4139

42-
// Flat config for ESLint v9+
43-
plugin.configs['flat/recommended'] = [
44-
{
45-
plugins: {
46-
'vue-modular': plugin,
47-
},
48-
rules: {
49-
'vue-modular/no-cross-feature-imports': 'error',
50-
'vue-modular/no-cross-module-imports': 'error',
51-
'vue-modular/no-business-logic-in-ui-kit': 'error',
52-
'vue-modular/enforce-import-boundaries': 'error',
53-
'vue-modular/enforce-src-structure': 'error',
54-
'vue-modular/enforce-app-structure': 'error',
55-
'vue-modular/enforce-module-exports': 'error',
56-
'vue-modular/enforce-feature-exports': 'error',
57-
'vue-modular/no-orphaned-files': 'error',
58-
'vue-modular/no-deep-nesting': 'warn',
59-
'vue-modular/enforce-sfc-order': 'error',
60-
},
61-
languageOptions: {
62-
ecmaVersion: 2022,
63-
sourceType: 'module',
64-
},
65-
},
66-
]
67-
68-
// Strict config with all rules enabled
69-
plugin.configs['flat/strict'] = [
70-
{
71-
plugins: {
72-
'vue-modular': plugin,
73-
},
74-
rules: {
75-
'vue-modular/no-cross-feature-imports': 'error',
76-
'vue-modular/no-cross-module-imports': 'error',
77-
'vue-modular/no-business-logic-in-ui-kit': 'error',
78-
'vue-modular/enforce-import-boundaries': 'error',
79-
'vue-modular/enforce-src-structure': 'error',
80-
'vue-modular/enforce-app-structure': 'error',
81-
'vue-modular/enforce-module-exports': 'error',
82-
'vue-modular/enforce-feature-exports': 'error',
83-
'vue-modular/enforce-naming-convention': 'error',
84-
'vue-modular/no-orphaned-files': 'error',
85-
'vue-modular/no-deep-nesting': 'error',
86-
'vue-modular/enforce-sfc-order': 'error',
87-
},
88-
languageOptions: {
89-
ecmaVersion: 2022,
90-
sourceType: 'module',
91-
},
92-
},
93-
]
94-
95-
// Classic config for legacy support
96-
plugin.configs.recommended = {
97-
plugins: ['vue-modular'],
98-
rules: {
99-
'vue-modular/no-cross-feature-imports': 'error',
100-
'vue-modular/no-cross-module-imports': 'error',
101-
'vue-modular/no-business-logic-in-ui-kit': 'error',
102-
'vue-modular/enforce-import-boundaries': 'error',
103-
'vue-modular/enforce-src-structure': 'error',
104-
'vue-modular/enforce-app-structure': 'error',
105-
'vue-modular/enforce-module-exports': 'error',
106-
'vue-modular/enforce-feature-exports': 'error',
107-
'vue-modular/no-orphaned-files': 'error',
108-
'vue-modular/no-deep-nesting': 'warn',
109-
'vue-modular/enforce-sfc-order': 'error',
110-
},
111-
}
112-
113-
// Strict config with all rules enabled (legacy)
114-
plugin.configs.strict = {
115-
plugins: ['vue-modular'],
116-
rules: {
117-
'vue-modular/no-cross-feature-imports': 'error',
118-
'vue-modular/no-cross-module-imports': 'error',
119-
'vue-modular/enforce-src-structure': 'error',
120-
'vue-modular/enforce-app-structure': 'error',
121-
'vue-modular/enforce-module-exports': 'error',
122-
'vue-modular/enforce-feature-exports': 'error',
123-
'vue-modular/enforce-import-boundaries': 'error',
124-
'vue-modular/enforce-naming-convention': 'error',
125-
'vue-modular/no-orphaned-files': 'error',
126-
'vue-modular/no-deep-nesting': 'error',
127-
'vue-modular/enforce-sfc-order': 'error',
128-
},
129-
}
40+
plugin.configs = createConfigs(plugin)
13041

13142
export default plugin

src/rules/file-component-naming.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Enforce PascalCase filenames for Vue component files
3+
*/
4+
5+
import { toPascalCase } from '../utils'
6+
import { minimatch } from 'minimatch'
7+
import path from 'path'
8+
import { parseRuleOptions } from '../utils/global-state'
9+
10+
const defaultOptions = {
11+
src: 'src', // Base source directory to enforce the rule in
12+
ignore: [], // Array of glob patterns to ignore
13+
}
14+
15+
export default {
16+
meta: {
17+
type: 'suggestion',
18+
docs: {
19+
description: 'Require Vue component filenames to be PascalCase',
20+
category: 'Stylistic Issues',
21+
recommended: false,
22+
},
23+
defaultOptions: [defaultOptions],
24+
schema: [
25+
{
26+
type: 'object',
27+
properties: {
28+
src: { type: 'string' },
29+
ignore: { type: 'array', items: { type: 'string' } },
30+
},
31+
additionalProperties: false,
32+
},
33+
],
34+
messages: {
35+
filenameNotPascal: 'Component filename "{{filename}}" should be PascalCase (e.g., "{{expected}}.vue").',
36+
},
37+
},
38+
39+
create(context) {
40+
const filename = context.getFilename()
41+
const { src, ignore } = parseRuleOptions(context, defaultOptions)
42+
43+
if (!filename || !filename.endsWith('.vue')) {
44+
return {}
45+
}
46+
47+
// Skip ignored patterns (match against absolute and project-relative paths)
48+
const rel = path.relative(process.cwd(), filename)
49+
const isIgnored = ignore.some((pattern) => minimatch(filename, pattern) || minimatch(rel, pattern))
50+
if (isIgnored) return {}
51+
52+
// If src option is provided, only apply inside that folder
53+
if (src) {
54+
const parts = rel.split(path.sep)
55+
if (!parts.includes(src)) return {}
56+
}
57+
58+
return {
59+
Program(node) {
60+
const base = filename.split('/').pop()
61+
const name = base.replace(/\.vue$/i, '')
62+
const expected = toPascalCase(name)
63+
64+
if (name !== expected) {
65+
context.report({ node, messageId: 'filenameNotPascal', data: { filename: base, expected } })
66+
}
67+
},
68+
}
69+
},
70+
}

src/utils/global-state.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export function parseRuleOptions(context, defaultOptions) {
6868
parsed.required = Array.isArray(options.required) && options.required.length > 0 ? options.required : defaultOptions.required
6969
}
7070

71+
// Parse ignore option with array validation
72+
if (defaultOptions.ignore !== undefined) {
73+
parsed.ignore = Array.isArray(options.ignore) ? options.ignore : defaultOptions.ignore
74+
}
75+
7176
return parsed
7277
}
7378

src/utils/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './strings.js'
2+
export * from './import-boundaries.js'
3+
export * from './global-state.js'

src/utils/strings.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* String utilities for tests
3+
*/
4+
export function toPascalCase(name) {
5+
return String(name)
6+
.replace(/(^|[-_\s]+)([a-zA-Z0-9])/g, (_m, _g, ch) => ch.toUpperCase())
7+
.replace(/[^a-zA-Z0-9]/g, '')
8+
}

0 commit comments

Comments
 (0)