Skip to content

Commit 49cdfdf

Browse files
committed
feat: lint rule for camelCase file names
1 parent 5aaf6ee commit 49cdfdf

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

eslint-rules/fileNaming.mjs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// @ts-check
2+
3+
/**
4+
* Custom ESLint rule to enforce camelCase file naming convention.
5+
* @type {import('eslint').Rule.RuleModule}
6+
*/
7+
export const fileNamingRule = {
8+
meta: {
9+
type: 'suggestion',
10+
docs: {
11+
description: 'Enforce camelCase file naming convention',
12+
recommended: true,
13+
},
14+
messages: {
15+
invalidFileName:
16+
"File name '{{fileName}}' should be camelCase. Suggested: '{{suggested}}'",
17+
},
18+
schema: [
19+
{
20+
type: 'object',
21+
properties: {
22+
ignore: {
23+
type: 'array',
24+
items: { type: 'string' },
25+
description: 'Regex patterns for file names to ignore',
26+
},
27+
},
28+
additionalProperties: false,
29+
},
30+
],
31+
},
32+
33+
create(context) {
34+
return {
35+
Program(node) {
36+
const filename = context.filename || context.getFilename();
37+
const options = context.options[0] || {};
38+
const ignorePatterns = (options.ignore || []).map(
39+
(/** @type {string} */ pattern) => new RegExp(pattern)
40+
);
41+
42+
// Extract just the file name from the full path
43+
const parts = filename.split('/');
44+
const fileName = parts[parts.length - 1];
45+
46+
// Skip non-TypeScript files
47+
if (!fileName.endsWith('.ts') && !fileName.endsWith('.tsx')) {
48+
return;
49+
}
50+
51+
// Check if file matches any ignore pattern
52+
for (const pattern of ignorePatterns) {
53+
if (pattern.test(fileName)) {
54+
return;
55+
}
56+
}
57+
58+
// Get the base name (without extension)
59+
// Handle test files: name.test.ts -> name
60+
const baseName = fileName
61+
.replace(/\.test\.ts$/, '')
62+
.replace(/\.spec\.ts$/, '')
63+
.replace(/\.test\.tsx$/, '')
64+
.replace(/\.spec\.tsx$/, '')
65+
.replace(/\.ts$/, '')
66+
.replace(/\.tsx$/, '');
67+
68+
// Get the extension for suggestion
69+
const extension = fileName.slice(baseName.length);
70+
71+
// Check if the base name follows camelCase
72+
// camelCase: starts with lowercase letter, then letters/numbers only
73+
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
74+
75+
if (!camelCaseRegex.test(baseName)) {
76+
const suggested = toCamelCase(baseName) + extension;
77+
78+
context.report({
79+
node,
80+
messageId: 'invalidFileName',
81+
data: {
82+
fileName,
83+
suggested,
84+
},
85+
});
86+
}
87+
},
88+
};
89+
},
90+
};
91+
92+
/**
93+
* Convert a string to camelCase
94+
* @param {string} str
95+
* @returns {string}
96+
*/
97+
function toCamelCase(str) {
98+
return str
99+
.split(/[-_.\s]+/)
100+
.map((word, index) => {
101+
if (index === 0) {
102+
return word.toLowerCase();
103+
}
104+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
105+
})
106+
.join('');
107+
}

eslint.config.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import eslint from '@eslint/js';
44
import tseslint from 'typescript-eslint';
55
import eslintConfigPrettier from 'eslint-config-prettier/flat';
66
import nodePlugin from 'eslint-plugin-n';
7+
import { fileNamingRule } from './eslint-rules/fileNaming.mjs';
8+
9+
// Local plugin for custom rules
10+
const localPlugin = {
11+
rules: {
12+
'file-naming': fileNamingRule,
13+
},
14+
};
715

816
export default tseslint.config(
917
eslint.configs.recommended,
@@ -13,11 +21,18 @@ export default tseslint.config(
1321
reportUnusedDisableDirectives: false
1422
},
1523
plugins: {
16-
n: nodePlugin
24+
n: nodePlugin,
25+
local: localPlugin
1726
},
1827
rules: {
1928
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
20-
'n/prefer-node-protocol': 'error'
29+
'n/prefer-node-protocol': 'error',
30+
'local/file-naming': ['warn', {
31+
ignore: [
32+
'^spec\\.types',
33+
'^types\\.'
34+
]
35+
}]
2136
}
2237
},
2338
{

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)